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:
parent
242bb6ffa2
commit
a65773338c
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
Reference in New Issue
Block a user