diff --git a/config/config.example.yml b/config/config.example.yml index 0692d29afc1..581fcf5aa9a 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -10,10 +10,13 @@ ui: port: 4000 # NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: / - # The rateLimiter settings limit each IP to a 'max' of 500 requests per 'windowMs' (1 minute). + # The rateLimiter settings limit each IP to a 'limit' of 500 requests per 'windowMs' (1 minute). rateLimiter: windowMs: 60000 # 1 minute - max: 500 # limit each IP to 500 requests per windowMs + limit: 500 # limit each IP to 500 requests per windowMs + # IPv6 subnet mask applied to IPv6 addresses. + # See: https://express-rate-limit.mintlify.app/reference/configuration#ipv6subnet + ipv6Subnet: 56 # Trust X-FORWARDED-* headers from proxies (default = true) useProxies: true diff --git a/package-lock.json b/package-lock.json index 4284cc41586..25dc2f7bbf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "deepmerge": "^4.3.1", "ejs": "^3.1.10", "express": "^4.21.2", - "express-rate-limit": "^5.1.3", + "express-rate-limit": "^8.0.1", "fast-json-patch": "^3.1.1", "filesize": "^10.1.6", "http-proxy-middleware": "^2.0.9", @@ -12280,9 +12280,31 @@ } }, "node_modules/express-rate-limit": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz", - "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==" + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", + "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } }, "node_modules/express-static-gzip": { "version": "2.2.0", diff --git a/package.json b/package.json index 5fc97ee5150..14cff27cdd8 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "deepmerge": "^4.3.1", "ejs": "^3.1.10", "express": "^4.21.2", - "express-rate-limit": "^5.1.3", + "express-rate-limit": "^8.0.1", "fast-json-patch": "^3.1.1", "filesize": "^10.1.6", "http-proxy-middleware": "^2.0.9", diff --git a/server.ts b/server.ts index cf21eda6af9..750c03e7cd5 100644 --- a/server.ts +++ b/server.ts @@ -179,10 +179,14 @@ export function app() { * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. */ if (hasValue((environment.ui as UIServerConfig).rateLimiter)) { - const RateLimit = require('express-rate-limit'); - const limiter = new RateLimit({ + const { rateLimit } = require('express-rate-limit') + const limiter = rateLimit({ windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, - max: (environment.ui as UIServerConfig).rateLimiter.max, + limit: (environment.ui as UIServerConfig).rateLimiter.limit, + standardHeaders: true, + legacyHeaders: false, + // don't log ERR_ERL_PERMISSIVE_TRUST_PROXY if we are trusting proxies + validate: {trustProxy: !environment.ui.useProxies}, }); server.use(limiter); } diff --git a/src/config/config.util.spec.ts b/src/config/config.util.spec.ts index dad16048c0e..d602a0fa34d 100644 --- a/src/config/config.util.spec.ts +++ b/src/config/config.util.spec.ts @@ -9,7 +9,8 @@ describe('Config Util', () => { const appConfig = new DefaultAppConfig(); expect(appConfig.cache.msToLive.default).toEqual(15 * 60 * 1000); // 15 minute expect(appConfig.ui.rateLimiter.windowMs).toEqual(1 * 60 * 1000); // 1 minute - expect(appConfig.ui.rateLimiter.max).toEqual(500); + expect(appConfig.ui.rateLimiter.limit).toEqual(500); + expect(appConfig.ui.rateLimiter.ipv6Subnet).toEqual(56); expect(appConfig.ui.useProxies).toEqual(true); expect(appConfig.submission.autosave.metadata).toEqual([]); @@ -23,7 +24,8 @@ describe('Config Util', () => { const rateLimiter = { windowMs: 5 * 50 * 1000, // 5 minutes - max: 1000, + limit: 1000, + ipv6Subnet: 56, }; appConfig.ui.rateLimiter = rateLimiter; @@ -47,7 +49,8 @@ describe('Config Util', () => { expect(environment.cache.msToLive.default).toEqual(msToLive); expect(environment.ui.rateLimiter.windowMs).toEqual(rateLimiter.windowMs); - expect(environment.ui.rateLimiter.max).toEqual(rateLimiter.max); + expect(environment.ui.rateLimiter.limit).toEqual(rateLimiter.limit); + expect(environment.ui.rateLimiter.ipv6Subnet).toEqual(rateLimiter.ipv6Subnet); expect(environment.ui.useProxies).toEqual(false); expect(environment.submission.autosave.metadata[0]).toEqual(autoSaveMetadata[0]); expect(environment.submission.autosave.metadata[1]).toEqual(autoSaveMetadata[1]); diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 4dadf443c65..15924fceb69 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -47,10 +47,11 @@ export class DefaultAppConfig implements AppConfig { // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: '/', - // The rateLimiter settings limit each IP to a 'max' of 500 requests per 'windowMs' (1 minute). + // The rateLimiter settings limit each IP to a 'limit' of 500 requests per 'windowMs' (1 minute). rateLimiter: { windowMs: 1 * 60 * 1000, // 1 minute - max: 500, // limit each IP to 500 requests per windowMs + limit: 500, // limit each IP to 500 requests per windowMs + ipv6Subnet: 56, // IPv6 subnet mask applied to IPv6 addresses }, // Trust X-FORWARDED-* headers from proxies diff --git a/src/config/ui-server-config.interface.ts b/src/config/ui-server-config.interface.ts index 70e2fa3e262..8bff0e21eb9 100644 --- a/src/config/ui-server-config.interface.ts +++ b/src/config/ui-server-config.interface.ts @@ -8,7 +8,8 @@ export class UIServerConfig extends ServerConfig { // rateLimiter is used to limit the amount of requests a user is allowed make in an amount of time, in order to prevent overloading the server rateLimiter?: { windowMs: number; - max: number; + limit: number; + ipv6Subnet: number; }; // Trust X-FORWARDED-* headers from proxies diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 500ec0f6d4f..36cd5da4966 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -45,10 +45,11 @@ export const environment: BuildConfig = { // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: '/angular-dspace', baseUrl: 'http://dspace.com/angular-dspace', - // The rateLimiter settings limit each IP to a 'max' of 500 requests per 'windowMs' (1 minute). + // The rateLimiter settings limit each IP to a 'limit' of 500 requests per 'windowMs' (1 minute). rateLimiter: { windowMs: 1 * 60 * 1000, // 1 minute - max: 500, // limit each IP to 500 requests per windowMs + limit: 500, // limit each IP to 500 requests per windowMs + ipv6Subnet: 56, }, useProxies: true, },