Skip to content

Commit a657733

Browse files
committed
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.
1 parent 242bb6f commit a657733

File tree

2 files changed

+36
-2
lines changed

2 files changed

+36
-2
lines changed

packages/server/src/cli.ts

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development")
3838
.option("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.")
3939
.option("--disable-telemetry", "Disables ALL telemetry.", false)
4040
.option("--socket <value>", "Listen on a UNIX socket. Host and port will be ignored when set.")
41+
.option("--trust-proxy", "Trust the X-Forwarded-For header, useful when using a reverse proxy.", false)
4142
.option("--install-extension <value>", "Install an extension by its ID.")
4243
.option("--bootstrap-fork <name>", "Used for development. Never set.")
4344
.option("--extra-args <args>", "Used for development. Never set.")
@@ -74,6 +75,7 @@ const bold = (text: string | number): string | number => {
7475
readonly cert?: string;
7576
readonly certKey?: string;
7677
readonly socket?: string;
78+
readonly trustProxy?: boolean;
7779

7880
readonly installExtension?: string;
7981

@@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => {
273275
},
274276
},
275277
password,
278+
trustProxy: options.trustProxy,
276279
httpsOptions: hasCustomHttps ? {
277280
key: certKeyData,
278281
cert: certData,

packages/server/src/server.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface CreateAppOptions {
3131
httpsOptions?: https.ServerOptions;
3232
allowHttp?: boolean;
3333
bypassAuth?: boolean;
34+
trustProxy?: boolean;
3435
}
3536

3637
export const createApp = async (options: CreateAppOptions): Promise<{
@@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{
6263
return true;
6364
};
6465

66+
const remoteAddress = (req: http.IncomingMessage): string | void => {
67+
let xForwardedFor = req.headers["x-forwarded-for"];
68+
if (Array.isArray(xForwardedFor)) {
69+
xForwardedFor = xForwardedFor.join(", ");
70+
}
71+
72+
if (options.trustProxy && xForwardedFor !== undefined) {
73+
const addresses = xForwardedFor.split(",").map(s => s.trim());
74+
75+
return addresses.pop();
76+
}
77+
78+
return req.socket.remoteAddress;
79+
};
80+
6581
const isAuthed = (req: http.IncomingMessage): boolean => {
6682
try {
6783
if (!options.password || options.bypassAuth) {
@@ -70,7 +86,20 @@ export const createApp = async (options: CreateAppOptions): Promise<{
7086

7187
// Try/catch placed here just in case
7288
const cookies = parseCookies(req);
73-
if (cookies.password && safeCompare(cookies.password, options.password)) {
89+
if (cookies.password) {
90+
if (!safeCompare(cookies.password, options.password)) {
91+
let userAgent = req.headers["user-agent"];
92+
if (Array.isArray(userAgent)) {
93+
userAgent = userAgent.join(", ");
94+
}
95+
logger.info("Failed login attempt",
96+
field("password", cookies.password),
97+
field("remote_address", remoteAddress(req)),
98+
field("user_agent", userAgent));
99+
100+
return false;
101+
}
102+
74103
return true;
75104
}
76105
} catch (ex) {
@@ -214,7 +243,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{
214243
const staticGzip = expressStaticGzip(path.join(baseDir, "build/web"));
215244

216245
app.use((req, res, next) => {
217-
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`, field("host", req.hostname), field("ip", req.ip));
246+
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.originalUrl}`,
247+
field("host", req.hostname),
248+
field("remote_address", remoteAddress(req)));
218249

219250
// Force HTTPS unless allowing HTTP.
220251
if (!isEncrypted(req.socket) && !options.allowHttp) {

0 commit comments

Comments
 (0)