From 5accf3fe5fe8f3430cf06f95adff18c4107756f7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 27 Apr 2020 08:41:52 -0400 Subject: [PATCH] Add basic rate limiting to login endpoint Closes #1320 --- .eslintrc.yaml | 1 + doc/FAQ.md | 2 ++ package.json | 1 + src/node/app/login.ts | 21 +++++++++++++++++++++ yarn.lock | 5 +++++ 5 files changed, 30 insertions(+) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 58112a671..06b284bde 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -21,3 +21,4 @@ extends: rules: # For overloads. no-dupe-class-members: off + "@typescript-eslint/no-use-before-define": off diff --git a/doc/FAQ.md b/doc/FAQ.md index 6da4fdcc7..ec0acf934 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -52,6 +52,8 @@ randomly generated password so you can use that. You can set the `PASSWORD` envi to use your own instead. If you want to handle authentication yourself, use `--auth none` to disable password authentication. +**note**: code-server will rate limit password authentication attempts at 2 a minute and 12 an hour. + If you want to use external authentication you should handle this with a reverse proxy using something like [oauth2_proxy](https://github.com/pusher/oauth2_proxy). diff --git a/package.json b/package.json index 0bf8d2a8d..b5c73a391 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "fs-extra": "^8.1.0", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", + "limiter": "^1.1.5", "node-pty": "^0.9.0", "pem": "^1.14.2", "safe-compare": "^1.1.4", diff --git a/src/node/app/login.ts b/src/node/app/login.ts index b55f5503c..c2dc707c2 100644 --- a/src/node/app/login.ts +++ b/src/node/app/login.ts @@ -1,4 +1,5 @@ import * as http from "http" +import * as limiter from "limiter" import * as querystring from "querystring" import { HttpCode, HttpError } from "../../common/http" import { AuthType, HttpProvider, HttpResponse, Route } from "../http" @@ -48,6 +49,8 @@ export class LoginHttpProvider extends HttpProvider { return this.replaceTemplates(route, response) } + private readonly limiter = new RateLimiter() + /** * Try logging in. On failure, show the login page with an error. */ @@ -59,6 +62,10 @@ export class LoginHttpProvider extends HttpProvider { } try { + if (!this.limiter.try()) { + throw new Error("Login rate limited!") + } + const data = await this.getData(request) const payload = data ? querystring.parse(data) : {} return await this.login(payload, route, request) @@ -108,3 +115,17 @@ export class LoginHttpProvider extends HttpProvider { throw new Error("Missing password") } } + +// RateLimiter wraps around the limiter library for logins. +// It allows 2 logins every minute and 12 logins every hour. +class RateLimiter { + private readonly minuteLimiter = new limiter.RateLimiter(2, "minute") + private readonly hourLimiter = new limiter.RateLimiter(12, "hour") + + public try(): boolean { + if (this.minuteLimiter.tryRemoveTokens(1)) { + return true + } + return this.hourLimiter.tryRemoveTokens(1) + } +} diff --git a/yarn.lock b/yarn.lock index c259a40e6..199d4aa84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4044,6 +4044,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +limiter@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"