DevSecOps - Mass Assignment
Tags: security, devops
Published: 2020-07-01 18:32:30

Seguindo a série de posts sobre segurança no processo de desenvolvimento de software, hoje o assunto abordado será **Mass Assignment**, ou atribuição em massa, que é um sub-tipo de exploração utilizando alteração de parâmetros.

A OWASP elencou este tipo de vulnerabilidade como a [6ª mais importante se tratando de segurança para APIs](https://apisecurity.io/encyclopedia/content/owasp/api6-mass-assignment.htm).

Conheci este tipo de exploração quando o Rails 4 foi lançado em 2013, e junto com ele o conceito de [Strong Parameters](https://github.com/rails/strong_parameters). Percebi que de lá pra cá muitos times de desenvolvimento deixam escapar verificações importantes de segurança que impediriam esse tipo de exploração. Por isso a importância de ser compartilhado.

#### Como funciona essa exploração

Basicamente, pessoas exploradoras podem tentar alterar valores críticos no seu banco de dados a partir de implementações no servidor que, ao evitar escrever códigos repetitivos, genericamente atualizam **todos campos que foram enviados na requisição**, sem verificações. Como exemplo, tomemos a seguinte estrutura de registro, para armazenar usuários em um sistema[^1]:

```javascript
{
  "id": String,
  "name": String,
  "department": String,
  "address": String,
  "mobile": String,
  "isAdmin": Boolean
}
```

Em poucas palavras, usuários possuem nome, departamento onde trabalham, endereço, número de celular e a informação se um usuário **é administrador do sistema ou não**, que é utilizada em verificações de segurança.

Digamos que uma das funcionalidades do sistema é a de poder **atualizar** os dados de um usuário, como **nome** (_name_), **endereço** (_address_) e **celular** (_mobile_).

Uma implementação **trivial**, para esta funcionalidade, seria:

```javascript
async function updateUser(id, newData) {
  try {
    await db.user.update(id, {
      name: newData.name,
      address: newData.address,
      mobile: newData.mobile
    })
  } catch (err) {
    console.error(err)
    return false
  }
  return true
}
```

Onde `newData` é um objeto com os valores **atualizados** dos campos de usuário, que provavelmente será enviado pela aplicação que controla a interface gráfica onde os usuários podem realizar as atualizações. Também estamos assumindo que `db.user.update` é uma função que atualiza o registro no banco de dados, de acordo com `id`. Foi realizado um tratamento mínimo de erro e, caso a operação não dê erro, retornamos `true` (caso contrário, `false`).

#### Onde começa o problema...

Muitos campos podem ser adicionados na estrutura de usuários, e com o passar do tempo esta rotina para atualizar os campos pode tornar-se repetitiva, exigindo que a cada inclusão de campo novo a função `updateUser` também seja alterada. Por este motivo, é comum que as pessoas desenvolvedoras tornem esta atualização "genérica", atualizando os registros de `user` independente dos campos que forem enviados em `newData`:

```javascript
async function updateUser(id, newData) {
  try {
    await db.user.update(id, newData)
  } catch (err) {
    console.error(err)
    return false
  }
  return true
}
```

Do ponto de vista de engenharia e design de código, a segunda versão do código parece mais robusta, certo? Certo.

Porém, o código acima abriu uma brecha crítica de segurança: uma tentativa mal-intencionada de atualizar estas informações pode colocar, também, o campo **isAdmin** no objeto `newData` (e defini-lo como `true`). Caso o sistema não se proteja em relação a isso, mesmo que a interface gráfica do usuário não permita a edição deste campo, seria possível tornar qualquer usuário administrador do sistema.

Em termos práticos, é possível "montar" uma requisição de atualização de campos para o servidor (com analisadores de rede ou até mesmo as ferramentas de desenvolvimento dos browsers) e, mesmo havendo autenticação no sistema, ainda assim seria possível alterar campos de acesso crítico.

Em outros exemplos, seria possível **zerar valores de produtos**, **alterar endereços de entrega de um pedido** entre outras alterações de impacto crítico.



#### Resolvendo de forma trivial, porém segura...

Caso não queiramos permitir que apenas o campo **isAdmin** seja alterado, vamos retirá-lo do objeto que enviamos para `db.user.update`:

```javascript
async function updateUser(id, newData) {
  try {
    // remove isAdmin from update using rest operator
    const { isAdmin, ...safeNewData } = newData
    await db.user.update(id, safeNewData)
  } catch (err) {
    console.error(err)
    return false
  }
  return true
}
```

Existem formas diferentes para remover um campo de um objeto, inclusive na mesma linguagem. No código acima, optei por utilizar o [operador rest](https://codeburst.io/use-es2015-object-rest-operator-to-omit-properties-38a3ecffe90) em uma [atribuição por desestruturação](https://javascript.info/destructuring-assignment), ambas construções do ECMAScript, para **criar um novo objeto sem o campo** `isAdmin`, em uma referência chamada `safeNewData`. Desta forma, respeitamos o [princípio da imutabilidade](https://en.wikipedia.org/wiki/Immutable_object), bem importante na programação funcional. Agora, independente dos valores que cheguem no objeto `newData`, o campo `isAdmin` não será alterado.

#### Tornando a solução segura e robusta...

Embora tenhamos resolvido o problema com segurança, agora temos dois _code smells_ no código: rigidez e fragilidade. Existe um grande risco de qualquer alteração na estrutura de `user` demandar alterações na função `updateUser`, aumentando o acoplamento e custo de manutenção do código. Novos campos críticos, que não devem ser mexidos pelos usuários, deverão ser manualmente removidos no código de `updateUser`.

Como solução geral, independente da forma como for implementado, podemos criar uma lista com os campos que **não podem** ser atualizados (a menos que o usuário possua permissão para tal), você pode chamar esta lista como *deny list*, ou *forbidden list*.

```javascript
const FORBIDDEN_FIELDS = [
  'id',
  'isAdmin'
]
```

Uma vez com a lista de campos que não podem ser alterados[^2], nosso código pode, antes de atualizar o registro no banco de dados, remover todos os campos que estão em `FORBIDDEN_FIELDS` e também no objeto com os dados da tentativa de atualização.

Versão imperativa:

```javascript
const maliciousUpdate = {
  name: 'New Name',
  address: 'New Address',
  isAdmin: true,
}

for (const [k, v] of Object.entries(maliciousUpdate)) {
  if (FORBIDDEN_FIELDS.includes(k)) {
    delete maliciousUpdate[k]
  }
}
console.log(maliciousUpdate)
// { name: "New Name", address: "New Address" }
```

Versão funcional:

```javascript
const removeUnsafeFields = obj => Object.entries(obj)
  .filter(([k,v]) => !FORBIDDEN_FIELDS.includes(k))
  .reduce((acc, [k,v]) => ({ ...acc, [k]: v }), {})

const safeUpdate = removeUnsafeFields(maliciousUpdate)
console.log(safeUpdate)
// { name: "New Name", address: "New Address" }
```

Explicando um pouco a versão funcional: primeiro filtramos todas as tuplas `[chave, valor]` do objeto e pegamos somente aquelas cujo campo não esteja na lista de proibição de atualização (`FORBIDDEN_FIELDS`), depois nós juntamos todas estas tuplas restantes a partir de um objeto vazio, com uma operação de redução.

Basta aplicar a função `removeUnsafeFields` em pontos do código onde exista atualização. Caso você precise ter proibições diferentes para estruturas diferentes, basta parametrizar uma lista de proibição na função `removeUnsafeFields`, invés de usar sempre a mesma referência para `FORBIDDEN_FIELDS`.

#### Unificando estas verificações no nível da aplicação

Se você utilizar frameworks de desenvolvimento, provavelmente eles permitem que você **execute código customizado toda vez que algum evento aconteça**. No exemplo abaixo, toda vez que uma web app Express.js recebe uma requisição HTTP e a ação for um `PATCH` (update), realizamos a verificação de segurança no campo `body` da requisição:

```javascript
const massAssignmentProtector = (req, res, next) => {
  if (req.method === 'PATCH') {
    const { body } = req
    req.body = removeUnsafeFields(body)
  }
  next()
}
app.use(massAssignmentProtector)
```

O exemplo completo, incluindo cenários demonstrando a brecha de segurança, encontra-se no replit abaixo:

#

{% replit @bernardobrezend/mass-assignment-attack-sample %}

#### Conclusões

1. Códigos genéricos, como essa atribuição em massa, tem pontos positivos (como a redução de _boilerplate_), mas podem expôr brechas de segurança. Por isso, **sempre questione o código também na dimensão da segurança**, pois ela é uma parte (bem) importante na qualidade do seu software.
2. Se preciso, implemente verificações por `roles` e permita que somente alguns perfis de usuários podem alterar todos os campos.
2. Se você possui campos sensíveis e que não podem ser alterados por interações com usuário (mas podem ser autocalculados ou coisas do tipo), nunca confie 100% nas atualizações ou operações que alguma camada externa manda para seu software. Sempre realize as suas verificações de segurança.

[^1]: a estrutura dos campos é meramente didática e não representa os padrões de modelagem e normalização de dados.
[^2]: em muitos bancos de dados alterar o campo `id` gera um erro, mas o exemplo é didático.