From 2324ddeb5c9df9e95da71339ce5fe71cd256e1cc Mon Sep 17 00:00:00 2001 From: ThomasTDS Date: Fri, 2 Oct 2026 22:01:16 -0300 Subject: [PATCH] feat: adiciona testes de seguranca inspirados no OWASP API Top 10 Adiciona 7 novos testes (TC-061 a TC-067): token invalido (nao so ausente) bloqueando POST/PUT/DELETE, observacao leve de rate limiting (rajada de 15 GETs simultaneos, sem flood), mass assignment (cliente tentando definir/alterar o campo id via POST e PUT) e tratamento seguro de caracteres especiais/script no corpo da requisicao. Recorte deliberadamente seguro: nenhum teste toca ou tenta acessar dado que nao foi criado pelo proprio teste, ja que a GoRest e um sandbox publico compartilhado. Categorias do OWASP Top 10 que exigiriam isso (ex.: autorizacao por nivel de funcao administrativa, que a GoRest nem possui) ficam fora de escopo. Documenta na issue #43 a ausencia de sinal de rate limiting observada, como caracteristica (nao defeito) da API de terceiros. --- README.md | 4 +++ docs/test-plan.md | 26 +++++++++++++++ tests/security.test.js | 44 +++++++++++++++++++++++++ tests/security.write.test.js | 64 ++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 tests/security.test.js create mode 100644 tests/security.write.test.js diff --git a/README.md b/README.md index 4a6d3d6..caf8754 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ Demonstrar, na prática: OpenAPI - já encontrou e documentou defeitos reais na API sob teste. - Relatórios de teste em **HTML**, gerados a cada execução do Jest e do Newman e publicados como artefato do CI. +- Testes de segurança inspirados no **OWASP API Security Top 10** + (autenticação inválida, mass assignment, tratamento de entrada, + observação de rate limiting), com um recorte que nunca toca dado de + terceiros nem faz varredura agressiva contra o sandbox público. ## Estrutura diff --git a/docs/test-plan.md b/docs/test-plan.md index 11a7ea7..fa6a504 100644 --- a/docs/test-plan.md +++ b/docs/test-plan.md @@ -173,6 +173,32 @@ defeito novo, documentado na [issue #9](https://github.com/ThomasTDS/qa-api-swag respostas `405 Method Not Allowed` sem o header `Allow` exigido pela RFC 9110, em 10 endpoints diferentes. +## Casos implementados - Segurança + +Cenários inspirados no OWASP API Security Top 10, com um recorte +deliberadamente seguro: a GoRest é uma API pública compartilhada por todo +mundo que usa a ferramenta, então nenhum teste aqui toca ou tenta acessar +dado que não foi criado pelo próprio teste, nem faz varredura agressiva +contra o serviço. Categorias do Top 10 que exigiriam isso (ex.: autorização +por nível de função administrativa, que a GoRest nem possui) ficam fora de +escopo. + +| ID | Endpoint | Cenário | Tipo | Resultado esperado | Arquivo | +| ------ | ---------------------------- | ------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- | +| TC-061 | POST /users | Token de autenticação inválido (não apenas ausente) | Negativo | 401 + `{"message":"Invalid token"}` (mensagem distinta de token ausente) | [security.test.js](../tests/security.test.js) | +| TC-062 | PUT /users/{id} | Token de autenticação inválido | Negativo | 401 | [security.test.js](../tests/security.test.js) | +| TC-063 | DELETE /users/{id} | Token de autenticação inválido | Negativo | 401 | [security.test.js](../tests/security.test.js) | +| TC-064 | GET /users | Rajada de 15 requisições simultâneas (observação de rate limiting) | Observacional | 200 em todas, sem header de rate limit nem 429 - ver issue documentada | [security.test.js](../tests/security.test.js) | +| TC-065 | POST /users | Cliente tenta definir o próprio `id` (mass assignment) | Negativo | 201 + `id` gerado pelo servidor, ignorando o valor enviado | [security.write.test.js](../tests/security.write.test.js) | +| TC-066 | PUT /users/{id} | Cliente tenta alterar o `id` de um registro existente (mass assignment) | Negativo | 200 + `id` original inalterado | [security.write.test.js](../tests/security.write.test.js) | +| TC-067 | POST /users | Caracteres especiais/script no campo `name` | Positivo | 201 + valor armazenado e devolvido integralmente, sem erro 500 | [security.write.test.js](../tests/security.write.test.js) | + +Todos os 7 casos (TC-061 a TC-067) foram executados com sucesso contra a API +real, incluindo os autenticados. O TC-064 documenta uma característica (não +necessariamente um defeito) na [issue #43](https://github.com/ThomasTDS/qa-api-swagger/issues/43): +a GoRest não expõe nenhum sinal de rate limiting em endpoints de leitura, +pelo menos até o volume testado. + ## Relatórios de teste (HTML) `npm test` e `npm run postman:run`/`postman:run:read-only` geram relatórios diff --git a/tests/security.test.js b/tests/security.test.js new file mode 100644 index 0000000..fd9ce55 --- /dev/null +++ b/tests/security.test.js @@ -0,0 +1,44 @@ +const { createApiClient } = require("../src/apiClient"); + +const api = createApiClient(); +const invalidAuthHeader = { Authorization: "Bearer token-invalido-garbage-123" }; + +describe("Segurança - autenticação inválida (não apenas ausente)", () => { + test("POST com token inválido retorna 401", async () => { + const res = await api.post( + "/users", + { name: "x", email: "x@example.test", gender: "male", status: "active" }, + { headers: invalidAuthHeader } + ); + + expect(res.status).toBe(401); + expect(res.data).toEqual({ message: "Invalid token" }); + }); + + test("PUT com token inválido retorna 401", async () => { + const res = await api.put("/users/1", { status: "inactive" }, { headers: invalidAuthHeader }); + + expect(res.status).toBe(401); + }); + + test("DELETE com token inválido retorna 401", async () => { + const res = await api.delete("/users/1", { headers: invalidAuthHeader }); + + expect(res.status).toBe(401); + }); +}); + +describe("Segurança - rate limiting (observação de característica)", () => { + test("uma rajada pequena de GETs não retorna 429 nem expõe headers de rate limit", async () => { + const burstSize = 15; + const requests = Array.from({ length: burstSize }, () => api.get("/users?page=1")); + const responses = await Promise.all(requests); + + expect(responses.every((res) => res.status === 200)).toBe(true); + + const hasRateLimitHeaders = responses.some((res) => + Object.keys(res.headers).some((h) => /ratelimit|retry-after/i.test(h)) + ); + expect(hasRateLimitHeaders).toBe(false); + }); +}); diff --git a/tests/security.write.test.js b/tests/security.write.test.js new file mode 100644 index 0000000..d75f337 --- /dev/null +++ b/tests/security.write.test.js @@ -0,0 +1,64 @@ +const { createApiClient } = require("../src/apiClient"); + +const describeIfToken = process.env.GOREST_TOKEN ? describe : describe.skip; + +describeIfToken("Segurança - mass assignment e tratamento de entrada (autenticado)", () => { + let api; + + beforeAll(() => { + api = createApiClient({ withAuth: true }); + }); + + function uniqueEmail(prefix) { + return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}@example.test`; + } + + test("POST /users ignora o campo 'id' enviado pelo cliente", async () => { + const res = await api.post("/users", { + id: 1, + name: "QA Portfolio - Mass Assignment", + email: uniqueEmail("mass-assign-post"), + gender: "male", + status: "active", + }); + + expect(res.status).toBe(201); + expect(res.data.id).not.toBe(1); + expect(res.data.id).toBeGreaterThan(1); + + await api.delete(`/users/${res.data.id}`); + }); + + test("PUT /users/{id} ignora tentativa de alterar o 'id'", async () => { + const created = await api.post("/users", { + name: "QA Portfolio - Mass Assignment PUT", + email: uniqueEmail("mass-assign-put"), + gender: "male", + status: "active", + }); + const originalId = created.data.id; + + const res = await api.put(`/users/${originalId}`, { id: 1 }); + + expect(res.status).toBe(200); + expect(res.data.id).toBe(originalId); + + await api.delete(`/users/${originalId}`); + }); + + test("POST /users com caracteres especiais/script no nome é armazenado e devolvido sem corromper a resposta", async () => { + const payload = { + name: " O'Brien --", + email: uniqueEmail("injection"), + gender: "male", + status: "active", + }; + + const res = await api.post("/users", payload); + + expect(res.status).toBe(201); + expect(res.data.name).toBe(payload.name); + + await api.delete(`/users/${res.data.id}`); + }); +});