diff --git a/packages/cli/package.json b/packages/cli/package.json index 651e2a3dee..27bab5ea18 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -129,6 +129,7 @@ "express-handlebars": "^7.0.2", "express-openapi-validator": "^4.13.6", "express-prom-bundle": "^6.6.0", + "express-rate-limit": "^7.1.3", "fast-glob": "^3.2.5", "flatted": "^3.2.4", "formidable": "^3.5.0", diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index 137f5f5de3..6fab9bb038 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -55,6 +55,9 @@ export abstract class AbstractServer { this.app = express(); this.app.disable('x-powered-by'); + const proxyHops = config.getEnv('proxy_hops'); + if (proxyHops > 0) this.app.set('trust proxy', proxyHops); + this.protocol = config.getEnv('protocol'); this.sslKey = config.getEnv('ssl_key'); this.sslCert = config.getEnv('ssl_cert'); diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 438a4daf0e..8ab9d0aa22 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -1344,4 +1344,11 @@ export const schema = { env: 'N8N_LEADER_SELECTION_CHECK_INTERVAL', }, }, + + proxy_hops: { + format: Number, + default: 0, + env: 'N8N_PROXY_HOPS', + doc: 'Number of reverse-proxies n8n is running behind', + }, }; diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index c8bf9ceeaa..ef25cbd826 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -24,12 +24,18 @@ import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { UserService } from '@/services/user.service'; import { License } from '@/License'; import { Container } from 'typedi'; -import { RESPONSE_ERROR_MESSAGES } from '@/constants'; +import { RESPONSE_ERROR_MESSAGES, inTest } from '@/constants'; import { TokenExpiredError } from 'jsonwebtoken'; import type { JwtPayload } from '@/services/jwt.service'; import { JwtService } from '@/services/jwt.service'; import { MfaService } from '@/Mfa/mfa.service'; import { Logger } from '@/Logger'; +import { rateLimit } from 'express-rate-limit'; + +const throttle = rateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + limit: 5, // Limit each IP to 5 requests per `window` (here, per 5 minutes). +}); @RestController() export class PasswordResetController { @@ -46,7 +52,9 @@ export class PasswordResetController { /** * Send a password reset email. */ - @Post('/forgot-password') + @Post('/forgot-password', { + middlewares: !inTest ? [throttle] : [], + }) async forgotPassword(req: PasswordResetRequest.Email) { if (!this.mailer.isEmailSetUp) { this.logger.debug( diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index be64fc1a3e..7b9c66e3b7 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -660,6 +660,7 @@ "forgotPassword.sendingEmailError": "Problem sending email", "forgotPassword.ldapUserPasswordResetUnavailable": "Please contact your LDAP administrator to reset your password", "forgotPassword.smtpErrorContactAdministrator": "Please contact your administrator (problem with your SMTP setup)", + "forgotPassword.tooManyRequests": "You’ve reached the password reset limit. Please try again in a few minutes.", "forms.resourceFiltersDropdown.filters": "Filters", "forms.resourceFiltersDropdown.ownedBy": "Owned by", "forms.resourceFiltersDropdown.sharedWith": "Shared with", diff --git a/packages/editor-ui/src/views/ForgotMyPasswordView.vue b/packages/editor-ui/src/views/ForgotMyPasswordView.vue index 55ede7a05a..eaae4a39f8 100644 --- a/packages/editor-ui/src/views/ForgotMyPasswordView.vue +++ b/packages/editor-ui/src/views/ForgotMyPasswordView.vue @@ -88,14 +88,20 @@ export default defineComponent({ }); } catch (error) { let message = this.$locale.baseText('forgotPassword.smtpErrorContactAdministrator'); - if (error.httpStatusCode === 422) { - message = this.$locale.baseText(error.message); + if (error.isAxiosError) { + const { status } = error.response; + if (status === 429) { + message = this.$locale.baseText('forgotPassword.tooManyRequests'); + } else if (error.httpStatusCode === 422) { + message = this.$locale.baseText(error.message); + } + + this.showMessage({ + type: 'error', + title: this.$locale.baseText('forgotPassword.sendingEmailError'), + message, + }); } - this.showMessage({ - type: 'error', - title: this.$locale.baseText('forgotPassword.sendingEmailError'), - message, - }); } this.loading = false; }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61c36655b3..fbaeec2849 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,9 @@ importers: express-prom-bundle: specifier: ^6.6.0 version: 6.6.0(prom-client@13.2.0) + express-rate-limit: + specifier: ^7.1.3 + version: 7.1.3(express@4.18.2) fast-glob: specifier: ^3.2.5 version: 3.2.12 @@ -9641,7 +9644,7 @@ packages: /axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@3.2.7) transitivePeerDependencies: - debug dev: false @@ -9670,11 +9673,12 @@ packages: form-data: 4.0.0 transitivePeerDependencies: - debug + dev: true /axios@1.4.0: resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@3.2.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -12772,6 +12776,15 @@ packages: url-value-parser: 2.2.0 dev: false + /express-rate-limit@7.1.3(express@4.18.2): + resolution: {integrity: sha512-BDes6WeNYSGRRGQU8QDNwUnwqaBro28HN/TTweM3RlxXRHDld8RLoH7tbfCxAc0hamQyn6aL0KrfR45+ZxknYg==} + engines: {node: '>= 16'} + peerDependencies: + express: 4 || 5 || ^5.0.0-beta.1 + dependencies: + express: 4.18.2 + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -13236,6 +13249,7 @@ packages: optional: true dependencies: debug: 4.3.4(supports-color@8.1.1) + dev: true /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -18476,7 +18490,7 @@ packages: resolution: {integrity: sha512-aXYe/D+28kF63W8Cz53t09ypEORz+ULeDCahdAqhVrRm2scbOXFbtnn0GGhvMpYe45grepLKuwui9KxrZ2ZuMw==} engines: {node: '>=14.17.0'} dependencies: - axios: 0.27.2(debug@4.3.4) + axios: 0.27.2(debug@3.2.7) transitivePeerDependencies: - debug dev: false