diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..c765cfe --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-01-21 - Fastify Rate Limiting & Dependency Conflict +**Vulnerability:** Rate limiting was completely disabled (commented out) in production code, exposing the API to DoS and brute-force attacks. Additionally, a major version mismatch in `@fastify/jwt` (v10 with Fastify v4) prevented the server from starting if dependencies were updated. +**Learning:** Security features commented out "for development" often make their way to production. Framework plugin version compatibility (Fastify v4 vs v5 ecosystem) can silently break applications or prevent security updates if not pinned correctly. The global error handler in Fastify can mask or transform expected error responses from security plugins like `rate-limit`. +**Prevention:** Use environment variables (e.g. `ENABLE_RATE_LIMIT=false`) instead of commenting out code. Strictly verify peer dependencies when upgrading packages. Ensure security tests verify the *actual* response structure, as global handlers may intercept plugin errors. diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json new file mode 100644 index 0000000..cc0fcb7 --- /dev/null +++ b/backend/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "no-console": "warn" + }, + "env": { + "node": true, + "jest": true + } +} diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000..644a81b --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + setupFiles: ['/jest.setup.js'], + testMatch: ['**/__tests__/**/*.test.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; diff --git a/backend/jest.setup.js b/backend/jest.setup.js new file mode 100644 index 0000000..92de43f --- /dev/null +++ b/backend/jest.setup.js @@ -0,0 +1,41 @@ +process.env.NODE_ENV = 'test'; +process.env.PORT = '3001'; +process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test'; +process.env.JWT_SECRET = 'test-secret-at-least-32-chars-long-should-be-here'; +process.env.LUNES_RPC_URL = 'http://localhost:9933'; +process.env.CONTRACT_ADDRESS = '0x123'; +process.env.PRIVATE_KEY = '0xabc'; +process.env.AWS_ACCESS_KEY_ID = 'test'; +process.env.AWS_SECRET_ACCESS_KEY = 'test'; +process.env.AWS_BUCKET_NAME = 'test'; +process.env.AWS_REGION = 'us-east-1'; + +// Mock Redis to prevent connection errors +jest.mock('ioredis', () => { + return jest.fn().mockImplementation(() => { + return { + get: jest.fn(), + set: jest.fn(), + del: jest.fn(), + on: jest.fn(), + quit: jest.fn(), + }; + }); +}); + +// Mock logger to avoid noise +jest.mock('./src/shared/logger', () => ({ + logger: { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, + Logger: { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + auth: jest.fn(), + } +})); diff --git a/backend/package-lock.json b/backend/package-lock.json index c5266d5..207b190 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^10.0.0", + "@fastify/jwt": "^8.0.1", "@fastify/multipart": "^8.0.0", "@fastify/rate-limit": "^9.0.1", "@fastify/swagger": "^8.12.0", @@ -1536,60 +1536,18 @@ } }, "node_modules/@fastify/jwt": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.0.0.tgz", - "integrity": "sha512-2Qka3NiyNNcsfejMUvyzot1T4UYIzzcbkFGDdVyrl344fRZ/WkD6VFXOoXhxe2Pzf3LpJNkoSxUM4Ru4DVgkYA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-8.0.1.tgz", + "integrity": "sha512-295bd7V6bDCnZOu8MAQgM6r7V1KILB+kdEq1q6nbHfXCnML569n7NSo3WzeLDG6IAqDl+Rhzi1vjxwaNHhRCBA==", "license": "MIT", "dependencies": { - "@fastify/error": "^4.2.0", - "@lukeed/ms": "^2.0.2", - "fast-jwt": "^6.0.2", - "fastify-plugin": "^5.0.1", + "@fastify/error": "^3.0.0", + "@lukeed/ms": "^2.0.0", + "fast-jwt": "^4.0.0", + "fastify-plugin": "^4.0.0", "steed": "^1.1.3" } }, - "node_modules/@fastify/jwt/node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/jwt/node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -6860,27 +6818,18 @@ "license": "MIT" }, "node_modules/fast-jwt": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.0.2.tgz", - "integrity": "sha512-dTF4bhYnuXhZYQUaxsHKqAyA5y/L/kQc4fUu0wQ0BSA0dMfcNrcv0aqR2YnVi4f7e1OnzDVU7sDsNdzl1O5EVA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-4.0.5.tgz", + "integrity": "sha512-QnpNdn0955GT7SlT8iMgYfhTsityUWysrQjM+Q7bGFijLp6+TNWzlbSMPvgalbrQGRg4ZaHZgMcns5fYOm5avg==", "license": "Apache-2.0", "dependencies": { - "@lukeed/ms": "^2.0.2", + "@lukeed/ms": "^2.0.1", "asn1.js": "^5.4.1", "ecdsa-sig-formatter": "^1.0.11", - "mnemonist": "^0.40.0" + "mnemonist": "^0.39.5" }, "engines": { - "node": ">=20" - } - }, - "node_modules/fast-jwt/node_modules/mnemonist": { - "version": "0.40.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", - "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.4" + "node": ">=16" } }, "node_modules/fast-levenshtein": { diff --git a/backend/package.json b/backend/package.json index 11cd17a..c46144a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,7 +38,7 @@ "dependencies": { "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^10.0.0", + "@fastify/jwt": "^8.0.1", "@fastify/multipart": "^8.0.0", "@fastify/rate-limit": "^9.0.1", "@fastify/swagger": "^8.12.0", diff --git a/backend/src/__tests__/security/rate_limit.test.ts b/backend/src/__tests__/security/rate_limit.test.ts new file mode 100644 index 0000000..0d2d202 --- /dev/null +++ b/backend/src/__tests__/security/rate_limit.test.ts @@ -0,0 +1,50 @@ +import { app } from '../../app'; +import { FastifyInstance } from 'fastify'; + +describe('Security: Rate Limiting', () => { + let server: FastifyInstance; + + beforeAll(async () => { + // Initialize the app (register middlewares and routes) + await app.initialize(); + server = app.server; + }); + + afterAll(async () => { + await app.stop(); + }); + + it('should limit requests when exceeding the threshold', async () => { + const limit = 100; // Default limit in env.config.ts + const extraRequests = 10; + const totalRequests = limit + extraRequests; + const url = '/api/v1/auth/nonce/0x1234567890123456789012345678901234567890'; + + // We expect the first 'limit' requests to succeed (200) + // And the subsequent requests to fail (429) if rate limiting is enabled. + + // Rate limiting is ENABLED, so we expect to hit 429 eventually. + + let rateLimitTriggered = false; + + for (let i = 0; i < totalRequests; i++) { + const response = await server.inject({ + method: 'GET', + url, + }); + + if (response.statusCode === 429) { + rateLimitTriggered = true; + // Verify response structure - expecting Global Error Handler format + const body = JSON.parse(response.payload); + expect(body.success).toBe(false); + expect(body.error.code).toBe(429); + expect(body.error.message).toMatch(/Muitas tentativas/); + break; + } + } + + // Assert that rate limit WAS triggered + expect(rateLimitTriggered).toBe(true); + }); +}); diff --git a/backend/src/app.ts b/backend/src/app.ts index 22bd2dd..a67b638 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -38,7 +38,7 @@ class App { this.setupErrorHandling(); } - private async initialize(): Promise { + public async initialize(): Promise { await this.setupMiddlewares(); await this.setupRoutes(); } @@ -64,17 +64,18 @@ class App { }, }); - // Rate Limiting - DESABILITADO PARA DESENVOLVIMENTO - // await this.server.register(rateLimit, { - // max: envConfig.RATE_LIMIT_MAX_REQUESTS, - // timeWindow: envConfig.RATE_LIMIT_WINDOW_MS, - // errorResponseBuilder: (request, context) => ({ - // code: 429, - // error: 'Rate Limit Exceeded', - // message: `Muitas tentativas. Tente novamente em ${Math.round(context.ttl / 1000)} segundos.`, - // expiresIn: context.ttl, - // }), - // }); + // Rate Limiting + await this.server.register(rateLimit, { + max: envConfig.RATE_LIMIT_MAX_REQUESTS, + timeWindow: envConfig.RATE_LIMIT_WINDOW_MS, + errorResponseBuilder: (request, context) => ({ + code: 429, + error: 'Rate Limit Exceeded', + message: `Muitas tentativas. Tente novamente em ${Math.round(context.ttl / 1000)} segundos.`, + statusCode: 429, + expiresIn: context.ttl, + }), + }); // Swagger Documentation if (envConfig.ENABLE_SWAGGER) {