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.
This commit is contained in:
Dean Sheather 2019-07-07 16:50:43 +10:00
parent 242bb6ffa2
commit a65773338c
No known key found for this signature in database
GPG Key ID: B574DF7CAFDCFAA3
2 changed files with 36 additions and 2 deletions

View File

@ -38,6 +38,7 @@ commander.version(process.env.VERSION || "development")
.option("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.") .option("-P, --password <value>", "DEPRECATED: Use the PASSWORD environment variable instead. Specify a password for authentication.")
.option("--disable-telemetry", "Disables ALL telemetry.", false) .option("--disable-telemetry", "Disables ALL telemetry.", false)
.option("--socket <value>", "Listen on a UNIX socket. Host and port will be ignored when set.") .option("--socket <value>", "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 <value>", "Install an extension by its ID.") .option("--install-extension <value>", "Install an extension by its ID.")
.option("--bootstrap-fork <name>", "Used for development. Never set.") .option("--bootstrap-fork <name>", "Used for development. Never set.")
.option("--extra-args <args>", "Used for development. Never set.") .option("--extra-args <args>", "Used for development. Never set.")
@ -74,6 +75,7 @@ const bold = (text: string | number): string | number => {
readonly cert?: string; readonly cert?: string;
readonly certKey?: string; readonly certKey?: string;
readonly socket?: string; readonly socket?: string;
readonly trustProxy?: boolean;
readonly installExtension?: string; readonly installExtension?: string;
@ -273,6 +275,7 @@ const bold = (text: string | number): string | number => {
}, },
}, },
password, password,
trustProxy: options.trustProxy,
httpsOptions: hasCustomHttps ? { httpsOptions: hasCustomHttps ? {
key: certKeyData, key: certKeyData,
cert: certData, cert: certData,

View File

@ -31,6 +31,7 @@ interface CreateAppOptions {
httpsOptions?: https.ServerOptions; httpsOptions?: https.ServerOptions;
allowHttp?: boolean; allowHttp?: boolean;
bypassAuth?: boolean; bypassAuth?: boolean;
trustProxy?: boolean;
} }
export const createApp = async (options: CreateAppOptions): Promise<{ export const createApp = async (options: CreateAppOptions): Promise<{
@ -62,6 +63,21 @@ export const createApp = async (options: CreateAppOptions): Promise<{
return true; 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 => { const isAuthed = (req: http.IncomingMessage): boolean => {
try { try {
if (!options.password || options.bypassAuth) { if (!options.password || options.bypassAuth) {
@ -70,7 +86,20 @@ export const createApp = async (options: CreateAppOptions): Promise<{
// Try/catch placed here just in case // Try/catch placed here just in case
const cookies = parseCookies(req); 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; return true;
} }
} catch (ex) { } catch (ex) {
@ -214,7 +243,9 @@ export const createApp = async (options: CreateAppOptions): Promise<{
const staticGzip = expressStaticGzip(path.join(baseDir, "build/web")); const staticGzip = expressStaticGzip(path.join(baseDir, "build/web"));
app.use((req, res, next) => { 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. // Force HTTPS unless allowing HTTP.
if (!isEncrypted(req.socket) && !options.allowHttp) { if (!isEncrypted(req.socket) && !options.allowHttp) {