From e13c3897fce2eabc481c356d084c63476378615b Mon Sep 17 00:00:00 2001 From: Felipe Barso <77860630+aprendendofelipe@users.noreply.github.com> Date: Tue, 20 Dec 2022 17:25:58 +0000 Subject: [PATCH] feat(rate limit env): use rate limit only for paths in environment variable --- infra/rate-limit.js | 8 + middleware.public.js | 2 +- models/user.js | 197 +----------------- models/validator.js | 185 ++++++++++++++++ pages/[username]/[slug]/index.public.js | 1 - pages/[username]/index.public.js | 1 - .../[username]/pagina/[page]/index.public.js | 1 - .../contents/[username]/[slug]/patch.test.js | 12 +- .../api/v1/users/[username]/patch.test.js | 6 +- tests/integration/api/v1/users/post.test.js | 6 +- 10 files changed, 211 insertions(+), 208 deletions(-) diff --git a/infra/rate-limit.js b/infra/rate-limit.js index ed74870c1..54e09259c 100644 --- a/infra/rate-limit.js +++ b/infra/rate-limit.js @@ -26,6 +26,8 @@ async function check(request) { const method = request.method; const path = request.nextUrl.pathname; const limit = getLimit(method, path, realIp); + if (!limit) return { success: true }; + let timeout; try { @@ -60,6 +62,7 @@ async function check(request) { function getLimit(method, path, ip) { const defaultLimits = { + rateLimitPaths: [], general: { requests: 1000, window: '5 m', @@ -96,6 +99,11 @@ function getLimit(method, path, ip) { const configurationFromEnvironment = process.env.RATE_LIMITS ? JSON.parse(process.env.RATE_LIMITS) : {}; const configuration = { ...defaultLimits, ...configurationFromEnvironment }; + + if (!configuration.rateLimitPaths.find((rateLimitPath) => path?.startsWith(rateLimitPath))) { + return; + } + const limitKey = configuration[`${method} ${path}`] ? `${method} ${path}` : 'general'; const limit = { diff --git a/middleware.public.js b/middleware.public.js index f90988579..a2bf0a5bf 100644 --- a/middleware.public.js +++ b/middleware.public.js @@ -6,7 +6,7 @@ import { UnauthorizedError } from '/errors/index.js'; import ip from 'models/ip.js'; export const config = { - matcher: ['/api/:path*'], + matcher: ['/((?!_next/static|va/|favicon|manifest).*)'], }; export async function middleware(request) { diff --git a/models/user.js b/models/user.js index 4f767dfa5..b3cfeb714 100644 --- a/models/user.js +++ b/models/user.js @@ -142,7 +142,6 @@ async function findOneById(userId) { async function create(postedUserData) { const validUserData = validatePostSchema(postedUserData); - checkBlockedUsernames(validUserData.username); await validateUniqueUsername(validUserData.username); await validateUniqueEmail(validUserData.email); await hashPasswordInObject(validUserData); @@ -191,191 +190,6 @@ function validatePostSchema(postedUserData) { return cleanValues; } -function checkBlockedUsernames(username) { - const blockedUsernames = [ - 'tabnew', - 'tabnews', - 'contato', - 'contatos', - 'moderador', - 'moderadores', - 'moderadora', - 'moderadoras', - 'moderadores', - 'moderacao', - 'alerta', - 'alertas', - 'dados', - 'status', - 'estatisticas', - 'analytics', - 'auth', - 'authentication', - 'autenticacao', - 'autorizacao', - 'loja', - 'log', - 'login', - 'logout', - 'avatar', - 'backup', - 'banner', - 'banners', - 'beta', - 'blog', - 'posts', - 'category', - 'categories', - 'categoria', - 'categorias', - 'tags', - 'grupo', - 'grupos', - 'checkout', - 'carrinho', - 'comentario', - 'comentarios', - 'comunidade', - 'comunidades', - 'vagas', - 'curso', - 'cursos', - 'sobre', - 'conta', - 'contas', - 'anuncio', - 'anuncios', - 'anuncie', - 'anunciar', - 'afiliado', - 'afiliados', - 'criar', - 'create', - 'postar', - 'post', - 'publicar', - 'publish', - 'editar', - 'editor', - 'edit', - 'configuracao', - 'configuracoes', - 'configurar', - 'configure', - 'config', - 'preferencias', - 'conta', - 'account', - 'dashboard', - 'sair', - 'deslogar', - 'desconectar', - 'discussao', - 'documentacao', - 'download', - 'downloads', - 'draft', - 'rascunho', - 'app', - 'apps', - 'admin', - 'administrator', - 'administrador', - 'administradora', - 'administradores', - 'administracao', - 'suporte', - 'support', - 'pesquisa', - 'sysadmin', - 'superuser', - 'sudo', - 'root', - 'user', - 'users', - 'rootuser', - 'guest', - 'anonymous', - 'faq', - 'tag', - 'tags', - 'hoje', - 'ontem', - 'pagina', - 'trending', - 'username', - 'usuario', - 'usuarios', - 'email', - 'password', - 'senha', - 'docs', - 'documentacao', - 'guidelines', - 'diretrizes', - 'ajuda', - 'imagem', - 'imagens', - 'convite', - 'convites', - 'toc', - 'terms', - 'termos', - 'termos-de-uso', - 'regras', - 'contrato', - 'cultura', - 'licenca', - 'rss', - 'newsletter', - 'newsletters', - 'notification', - 'notifications', - 'notificacoes', - 'popular', - 'cadastro', - 'cadastrar', - 'interface', - 'recentes', - 'register', - 'registration', - 'resposta', - 'respostas', - 'replies', - 'reply', - 'relatorio', - 'relatorios', - 'resetar', - 'resetar-senha', - 'ceo', - 'cfo', - 'cto', - 'gerente', - 'membership', - 'news', - 'api', - 'css', - 'init', - 'museu', - 'upgrade', - 'features', - 'me', - 'perfil', - 'eu', - 'videos', - ]; - - if (blockedUsernames.includes(username.toLowerCase())) { - throw new ValidationError({ - message: `Este nome de usuário não está disponível para uso.`, - action: 'Escolha outro nome de usuário e tente novamente.', - stack: new Error().stack, - errorLocationCode: 'MODEL:USER:CHECK_BLOCKED_USERNAMES:BLOCKED_USERNAME', - key: 'username', - }); - } -} - // TODO: Refactor the interface of this function // and the code inside to make it more future proof // and to accept update using "userId". @@ -383,12 +197,11 @@ async function update(username, postedUserData, options = {}) { const validPostedUserData = await validatePatchSchema(postedUserData); const currentUser = await findOneByUsername(username); - if ('username' in validPostedUserData) { - checkBlockedUsernames(validPostedUserData.username); - - if (currentUser.username.toLowerCase() !== validPostedUserData.username.toLowerCase()) { - await validateUniqueUsername(validPostedUserData.username); - } + if ( + 'username' in validPostedUserData && + currentUser.username.toLowerCase() !== validPostedUserData.username.toLowerCase() + ) { + await validateUniqueUsername(validPostedUserData.username); } if ('email' in validPostedUserData) { diff --git a/models/validator.js b/models/validator.js index dfb0239d4..128ccaf2f 100644 --- a/models/validator.js +++ b/models/validator.js @@ -1,6 +1,7 @@ import Joi from 'joi'; import { ValidationError } from 'errors/index.js'; import removeMarkdown from 'models/remove-markdown'; +import webserver from 'infra/webserver'; export default function validator(object, keys) { // Force the clean up of "undefined" values since JSON @@ -75,6 +76,7 @@ const schemas = { .max(30) .trim() .invalid(null) + .custom(checkReservedUsernames, 'check if username is reserved') .when('$required.username', { is: 'required', then: Joi.required(), otherwise: Joi.optional() }) .messages({ 'any.required': `"username" é um campo obrigatório.`, @@ -84,6 +86,7 @@ const schemas = { 'string.min': `"username" deve conter no mínimo {#limit} caracteres.`, 'string.max': `"username" deve conter no máximo {#limit} caracteres.`, 'any.invalid': `"username" possui o valor inválido "null".`, + 'username.reserved': `Este nome de usuário não está disponível para uso.`, }), }); }, @@ -796,3 +799,185 @@ const schemas = { const withoutMarkdown = (value, helpers) => { return removeMarkdown(value, { trim: true }).length > 0 ? value : helpers.error('markdown.empty'); }; + +function checkReservedUsernames(username, helpers) { + if ( + (webserver.isLambdaServer() && reservedDevUsernames.includes(username.toLowerCase())) || + reservedUsernames.includes(username.toLowerCase()) || + reservedUsernamesStartingWith.find((reserved) => username.toLowerCase().startsWith(reserved)) + ) { + return helpers.error('username.reserved'); + } + return username; +} + +const reservedDevUsernames = ['admin', 'user']; +const reservedUsernamesStartingWith = ['favicon', 'manifest']; +const reservedUsernames = [ + 'account', + 'administracao', + 'administrador', + 'administradora', + 'administradores', + 'administrator', + 'afiliado', + 'afiliados', + 'ajuda', + 'alerta', + 'alertas', + 'analytics', + 'anonymous', + 'anunciar', + 'anuncie', + 'anuncio', + 'anuncios', + 'api', + 'app', + 'apps', + 'autenticacao', + 'auth', + 'authentication', + 'autorizacao', + 'avatar', + 'backup', + 'banner', + 'banners', + 'beta', + 'blog', + 'cadastrar', + 'cadastro', + 'carrinho', + 'categoria', + 'categorias', + 'categories', + 'category', + 'ceo', + 'cfo', + 'checkout', + 'comentario', + 'comentarios', + 'comunidade', + 'comunidades', + 'config', + 'configuracao', + 'configuracoes', + 'configurar', + 'configure', + 'conta', + 'contas', + 'contato', + 'contatos', + 'contrato', + 'convite', + 'convites', + 'create', + 'criar', + 'css', + 'cto', + 'cultura', + 'curso', + 'cursos', + 'dados', + 'dashboard', + 'desconectar', + 'deslogar', + 'diretrizes', + 'discussao', + 'docs', + 'documentacao', + 'download', + 'downloads', + 'draft', + 'edit', + 'editar', + 'editor', + 'email', + 'estatisticas', + 'eu', + 'faq', + 'features', + 'gerente', + 'grupo', + 'grupos', + 'guest', + 'guidelines', + 'hoje', + 'imagem', + 'imagens', + 'init', + 'interface', + 'licenca', + 'log', + 'login', + 'logout', + 'loja', + 'me', + 'membership', + 'moderacao', + 'moderador', + 'moderadora', + 'moderadoras', + 'moderadores', + 'museu', + 'news', + 'newsletter', + 'newsletters', + 'notificacoes', + 'notification', + 'notifications', + 'ontem', + 'pagina', + 'password', + 'perfil', + 'pesquisa', + 'popular', + 'post', + 'postar', + 'posts', + 'preferencias', + 'public', + 'publicar', + 'publish', + 'rascunho', + 'recentes', + 'register', + 'registration', + 'regras', + 'relatorio', + 'relatorios', + 'replies', + 'reply', + 'resetar-senha', + 'resetar', + 'resposta', + 'respostas', + 'root', + 'rootuser', + 'rss', + 'sair', + 'senha', + 'sobre', + 'status', + 'sudo', + 'superuser', + 'suporte', + 'support', + 'sysadmin', + 'tabnew', + 'tabnews', + 'tag', + 'tags', + 'termos-de-uso', + 'termos', + 'terms', + 'toc', + 'trending', + 'upgrade', + 'username', + 'users', + 'usuario', + 'usuarios', + 'va', + 'vagas', + 'videos', +]; diff --git a/pages/[username]/[slug]/index.public.js b/pages/[username]/[slug]/index.public.js index 3c631dc51..2fe71555b 100644 --- a/pages/[username]/[slug]/index.public.js +++ b/pages/[username]/[slug]/index.public.js @@ -297,7 +297,6 @@ export async function getStaticProps(context) { } catch (error) { return { notFound: true, - revalidate: 1, }; } diff --git a/pages/[username]/index.public.js b/pages/[username]/index.public.js index af1a6b457..fe61b5311 100644 --- a/pages/[username]/index.public.js +++ b/pages/[username]/index.public.js @@ -182,7 +182,6 @@ export async function getStaticProps(context) { } catch (error) { return { notFound: true, - revalidate: 1, }; } diff --git a/pages/[username]/pagina/[page]/index.public.js b/pages/[username]/pagina/[page]/index.public.js index ba2b3f5d3..9788ede1a 100644 --- a/pages/[username]/pagina/[page]/index.public.js +++ b/pages/[username]/pagina/[page]/index.public.js @@ -41,7 +41,6 @@ export async function getStaticProps(context) { console.log(error); return { notFound: true, - revalidate: 1, }; } diff --git a/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js b/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js index 72661b505..5aa6d9d51 100644 --- a/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js +++ b/tests/integration/api/v1/contents/[username]/[slug]/patch.test.js @@ -11,7 +11,7 @@ beforeAll(async () => { describe('PATCH /api/v1/contents/[username]/[slug]', () => { describe('Anonymous user', () => { test('Content with minimum valid data', async () => { - const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/username/slug`, { + const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/someUsername/slug`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', @@ -42,7 +42,7 @@ describe('PATCH /api/v1/contents/[username]/[slug]', () => { await orchestrator.removeFeaturesFromUser(userWithoutFeature, ['update:content']); const sessionObject = await orchestrator.createSession(userWithoutFeature); - const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/username/slug`, { + const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/${userWithoutFeature.username}/slug`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', @@ -117,7 +117,7 @@ describe('PATCH /api/v1/contents/[username]/[slug]', () => { await orchestrator.activateUser(defaultUser); const sessionObject = await orchestrator.createSession(defaultUser); - const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/username/slug`, { + const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents//${defaultUser.username}/slug`, { method: 'PATCH', headers: { cookie: `session_id=${sessionObject.token}`, @@ -141,7 +141,7 @@ describe('PATCH /api/v1/contents/[username]/[slug]', () => { await orchestrator.activateUser(defaultUser); const sessionObject = await orchestrator.createSession(defaultUser); - const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/username/slug`, { + const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/${defaultUser.username}/slug`, { method: 'PATCH', headers: { cookie: `session_id=${sessionObject.token}`, @@ -166,7 +166,7 @@ describe('PATCH /api/v1/contents/[username]/[slug]', () => { await orchestrator.activateUser(defaultUser); const sessionObject = await orchestrator.createSession(defaultUser); - const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/username/slug`, { + const response = await fetch(`${orchestrator.webserverUrl}/api/v1/contents/${defaultUser.username}/slug`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', @@ -219,7 +219,7 @@ describe('PATCH /api/v1/contents/[username]/[slug]', () => { const sessionObject = await orchestrator.createSession(defaultUser); const response = await fetch( - `${orchestrator.webserverUrl}/api/v1/contents/username/%3Cscript%3Ealert%28%29%3Cscript%3E`, + `${orchestrator.webserverUrl}/api/v1/contents/${defaultUser.username}/%3Cscript%3Ealert%28%29%3Cscript%3E`, { method: 'PATCH', headers: { diff --git a/tests/integration/api/v1/users/[username]/patch.test.js b/tests/integration/api/v1/users/[username]/patch.test.js index ef689abe4..0bf111c90 100644 --- a/tests/integration/api/v1/users/[username]/patch.test.js +++ b/tests/integration/api/v1/users/[username]/patch.test.js @@ -453,7 +453,7 @@ describe('PATCH /api/v1/users/[username]', () => { }, body: JSON.stringify({ - username: 'admin', + username: 'account', }), }); @@ -463,10 +463,10 @@ describe('PATCH /api/v1/users/[username]', () => { expect(responseBody.status_code).toEqual(400); expect(responseBody.name).toEqual('ValidationError'); expect(responseBody.message).toEqual('Este nome de usuário não está disponível para uso.'); - expect(responseBody.action).toEqual('Escolha outro nome de usuário e tente novamente.'); + expect(responseBody.action).toEqual('Ajuste os dados enviados e tente novamente.'); expect(uuidVersion(responseBody.error_id)).toEqual(4); expect(uuidVersion(responseBody.request_id)).toEqual(4); - expect(responseBody.error_location_code).toEqual('MODEL:USER:CHECK_BLOCKED_USERNAMES:BLOCKED_USERNAME'); + expect(responseBody.error_location_code).toEqual('MODEL:VALIDATOR:FINAL_SCHEMA'); expect(responseBody.key).toEqual('username'); }); diff --git a/tests/integration/api/v1/users/post.test.js b/tests/integration/api/v1/users/post.test.js index e80b4a667..039c9d296 100644 --- a/tests/integration/api/v1/users/post.test.js +++ b/tests/integration/api/v1/users/post.test.js @@ -365,7 +365,7 @@ describe('POST /api/v1/users', () => { 'Content-Type': 'application/json', }, body: JSON.stringify({ - username: 'admin', + username: 'administrator', email: 'admin@email.com', password: 'validpassword123', }), @@ -377,10 +377,10 @@ describe('POST /api/v1/users', () => { expect(responseBody.status_code).toEqual(400); expect(responseBody.name).toEqual('ValidationError'); expect(responseBody.message).toEqual('Este nome de usuário não está disponível para uso.'); - expect(responseBody.action).toEqual('Escolha outro nome de usuário e tente novamente.'); + expect(responseBody.action).toEqual('Ajuste os dados enviados e tente novamente.'); expect(uuidVersion(responseBody.error_id)).toEqual(4); expect(uuidVersion(responseBody.request_id)).toEqual(4); - expect(responseBody.error_location_code).toEqual('MODEL:USER:CHECK_BLOCKED_USERNAMES:BLOCKED_USERNAME'); + expect(responseBody.error_location_code).toEqual('MODEL:VALIDATOR:FINAL_SCHEMA'); expect(responseBody.key).toEqual('username'); });