diff --git a/bun.lock b/bun.lock index 42f1e81f..fcec7772 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "csrf": "^3.1.0", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-rate-limit": "^8.3.1", "form-data": "^4.0.6", "https-proxy-agent": "^7.0.6", "ioredis": "^5.6.1", @@ -212,6 +213,8 @@ "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], diff --git a/package.json b/package.json index db680bcc..5e7577c8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "csrf": "^3.1.0", "dotenv": "^16.4.7", "express": "^4.21.2", + "express-rate-limit": "^8.3.1", "form-data": "^4.0.6", "https-proxy-agent": "^7.0.6", "ioredis": "^5.6.1", diff --git a/src/routes/chat.js b/src/routes/chat.js index f36928f8..0e930448 100644 --- a/src/routes/chat.js +++ b/src/routes/chat.js @@ -1,4 +1,5 @@ const express = require('express') +const rateLimit = require('express-rate-limit') const router = express.Router() const { apiKeyVerify } = require('../middlewares/authorization.js') const { createUploadMiddleware } = require('../middlewares/upload.js') @@ -13,6 +14,17 @@ const { const parseMediaUpload = createUploadMiddleware() +// Limita cada API key a un maximo de peticiones por minuto en los endpoints +// costosos (chat/imagenes/video) para evitar el agotamiento de recursos por +// clientes autenticados de alto volumen (CWE-770). +const heavyEndpointLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => req.apiKey || req.ip +}) + const selectChatCompletion = (req, res, next) => { const ChatCompletionMap = { 't2t': handleChatCompletion, @@ -34,23 +46,27 @@ const selectChatCompletion = (req, res, next) => { router.post('/v1/chat/completions', apiKeyVerify, + heavyEndpointLimiter, processRequestBody, selectChatCompletion ) router.post('/v1/images/generations', apiKeyVerify, + heavyEndpointLimiter, handleOpenAIImagesGeneration ) router.post('/v1/images/edits', apiKeyVerify, + heavyEndpointLimiter, parseMediaUpload, handleOpenAIImagesEdit ) router.post('/v1/videos', apiKeyVerify, + heavyEndpointLimiter, parseMediaUpload, handleOpenAIVideoGeneration )