From a65773338c2bf140a4731a0ac10ac80694b6a831 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 7 Jul 2019 16:50:43 +1000 Subject: [PATCH] add failed authentication attempt logger When `isAuthed()` is called and the password cookie is not what we expected, the failed login attempt is logged with the provided password, remote address and user agent. To allow for logging failed attempts with a reverse proxy, the `--trust-proxy` argument has been added to trust the `X-Forwarded-For` header. This implementation of an `X-Forwarded-For` parser uses the last value in the list, therefore only trusting the nearest proxy. --- packages/server/src/cli.ts | 3 +++ packages/server/src/server.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 8433a403..7f43212a 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development") .option("-P, --password ", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.") .option("--disable-telemetry", "Disables ALL telemetry.", false) .option("--socket ", "Listen on a UNIX socket. Host and port will be ignored when set.") + .option("--trust-proxy", "Trust the X-Forwarded-For header, useful when using a reverse proxy.", false) .option("--install-extension ", "Install an extension by its ID.") .option("--bootstrap-fork ", "Used for development. Never set.") .option("--extra-args ", "Used for development. Never set.") @@ -74,6 +75,7 @@ const bold = (text: string | number): string | number => { readonly cert?: string; readonly certKey?: string; readonly socket?: string; + readonly trustProxy?: boolean; readonly installExtension?: string; @@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => { }, }, password, + trustProxy: options.trustProxy, httpsOptions: hasCustomHttps ? { key: certKeyData, cert: certData, diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index b7d9a12c..4c73743a 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -31,6 +31,7 @@ interface CreateAppOptions { httpsOptions?: https.ServerOptions; allowHttp?: boolean; bypassAuth?: boolean; + trustProxy?: boolean; } export const createApp = async (options: CreateAppOptions): Promise<{ @@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{ return true; }; + const remoteAddress = (req: http.IncomingMessage): string | void => { + let xForwardedFor = req.headers["x-forwarded-for"]; + if (Array.isArray(xForwardedFor)) { + xForwardedFor = xForwardedFor.join(", "); + } + + if (options.trustProxy && xForwardedFor !== undefined) { + const addresses = xForwardedFor.split(",").map(s => s.trim()); + + return addresses.pop(); + } + + return req.socket.remoteAddress; + }; + const isAuthed = (req: http.IncomingMessage): boolean => { try { if (!options.password || options.bypassAuth) { @@ -70,7 +86,20 @@ export const createApp = async (options: CreateAppOptions): Promise<{ // Try/catch placed here just in case const cookies = parseCookies(req); - if (cookies.password && safeCompare(cookies.password, options.password)) { + if (cookies.password) { + if (!safeCompare(cookies.password, options.password)) { + let userAgent = req.headers["user-agent"]; + if (Array.isArray(userAgent)) { + userAgent = userAgent.join(", "); + } + logger.info("Failed login attempt", + field("password", cookies.password), + field("remote_address", remoteAddress(req)), + field("user_agent", userAgent)); + + return false; + } + return true; } } catch (ex) { @@ -214,7 +243,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{ const staticGzip = expressStaticGzip(path.join(baseDir, "build/web")); app.use((req, res, next) => { - logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, field("host", req.hostname), field("ip", req.ip)); + logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, + field("host", req.hostname), + field("remote_address", remoteAddress(req))); // Force HTTPS unless allowing HTTP. if (!isEncrypted(req.socket) && !options.allowHttp) {