diff --git a/stellar-payment-platform/server.js b/stellar-payment-platform/server.js index c55c098e..92db54c9 100644 --- a/stellar-payment-platform/server.js +++ b/stellar-payment-platform/server.js @@ -202,6 +202,21 @@ const limiter = rateLimit({ }, }); +// Per-IP limiter specifically for sensitive, unauthenticated endpoints. +// Keys strictly by client IP so brute-force/spam from a single source is +// blocked regardless of how many account ids are rotated in the payload. +const ipLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + store: redisClient ? new RedisStore({ + sendCommand: (...args) => redisClient.sendCommand(args), + }) : undefined, + standardHeaders: true, + legacyHeaders: true, + message: errorBody('RATE_LIMITED', 'Too many requests, please try again later.'), + keyGenerator: (req) => req.ip || (req.connection && req.connection.remoteAddress) || '', +}); + app.use(cors(corsOptions)); app.use(express.json()); @@ -383,7 +398,7 @@ app.get('/metrics', async (req, res) => { } }); -app.get('/federation', etagCache, validateSchema({ query: federationQuerySchema }), async (req, res, next) => { +app.get('/federation', ipLimiter, etagCache, validateSchema({ query: federationQuerySchema }), async (req, res, next) => { const { q: queryValue, type } = req.query; try { @@ -564,7 +579,7 @@ const verifyFreighterRegistrationSignature = ({ * - Validates that provided signature(s) meet minimum threshold * - Ensures authorization requirements are satisfied */ -app.post('/register', idempotencyMiddleware(redisClient), requireJson, validateSchema({ body: registerBodySchema }), async (req, res, next) => { +app.post('/register', ipLimiter, idempotencyMiddleware(redisClient), requireJson, validateSchema({ body: registerBodySchema }), async (req, res, next) => { // registerBodySchema has already guaranteed that username is a trimmed // 3-20 character alphanumeric string and address is a non-empty trimmed // string, so those shape checks are not repeated here.