Hash password

Fixes issues with unexpected characters breaking things when setting the
cookie (like semicolons).

This change as-is does not affect the security of code-server
itself (we've just replaced the static password with a static hash) but
if we were to add a salt in the future it would let us invalidate keys
by rehashing with a new salt which could be handy.
This commit is contained in:
Asher 2019-11-07 15:43:10 -06:00
parent a1d6bcb8e5
commit 2018024810
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
2 changed files with 25 additions and 14 deletions

View File

@ -63,7 +63,7 @@ import { TelemetryClient } from "vs/server/src/node/insights";
import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls"; import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls";
import { Protocol } from "vs/server/src/node/protocol"; import { Protocol } from "vs/server/src/node/protocol";
import { UpdateService } from "vs/server/src/node/update"; import { UpdateService } from "vs/server/src/node/update";
import { AuthType, getMediaMime, getUriTransformer, localRequire, tmpdir } from "vs/server/src/node/util"; import { AuthType, getMediaMime, getUriTransformer, hash, localRequire, tmpdir } from "vs/server/src/node/util";
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService"; import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api"; import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
@ -98,7 +98,11 @@ export interface Response {
} }
export interface LoginPayload { export interface LoginPayload {
password?: string[] | string; password?: string;
}
export interface AuthPayload {
key?: string[];
} }
export class HttpError extends Error { export class HttpError extends Error {
@ -137,6 +141,7 @@ export abstract class Server {
host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost", host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
...options, ...options,
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "", basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
password: options.password ? hash(options.password) : undefined,
}; };
this.protocol = this.options.cert ? "https" : "http"; this.protocol = this.options.cert ? "https" : "http";
if (this.protocol === "https") { if (this.protocol === "https") {
@ -357,11 +362,11 @@ export abstract class Server {
} }
private async tryLogin(request: http.IncomingMessage): Promise<Response> { private async tryLogin(request: http.IncomingMessage): Promise<Response> {
const redirect = (password?: string | string[] | true) => { const redirect = (password: string | true) => {
return { return {
redirect: "/", redirect: "/",
headers: typeof password === "string" headers: typeof password === "string"
? { "Set-Cookie": `password=${password}; Path=${this.options.basePath || "/"}; HttpOnly; SameSite=strict` } ? { "Set-Cookie": `key=${password}; Path=${this.options.basePath || "/"}; HttpOnly; SameSite=strict` }
: {}, : {},
}; };
}; };
@ -371,8 +376,11 @@ export abstract class Server {
} }
if (request.method === "POST") { if (request.method === "POST") {
const data = await this.getData<LoginPayload>(request); const data = await this.getData<LoginPayload>(request);
if (this.authenticate(request, data)) { const password = this.authenticate(request, {
return redirect(data.password); key: typeof data.password === "string" ? [hash(data.password)] : undefined,
});
if (password) {
return redirect(password);
} }
console.error("Failed login attempt", JSON.stringify({ console.error("Failed login attempt", JSON.stringify({
xForwardedFor: request.headers["x-forwarded-for"], xForwardedFor: request.headers["x-forwarded-for"],
@ -432,19 +440,18 @@ export abstract class Server {
: Promise.resolve({} as T); : Promise.resolve({} as T);
} }
private authenticate(request: http.IncomingMessage, payload?: LoginPayload): string | boolean { private authenticate(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
if (this.options.auth !== "password") { if (this.options.auth === "none") {
return true; return true;
} }
const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index"); const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
if (typeof payload === "undefined") { if (typeof payload === "undefined") {
payload = this.parseCookies<LoginPayload>(request); payload = this.parseCookies<AuthPayload>(request);
} }
if (this.options.password && payload.password) { if (this.options.password && payload.key) {
const toTest = Array.isArray(payload.password) ? payload.password : [payload.password]; for (let i = 0; i < payload.key.length; ++i) {
for (let i = 0; i < toTest.length; ++i) { if (safeCompare(payload.key[i], this.options.password)) {
if (safeCompare(toTest[i], this.options.password)) { return payload.key[i];
return toTest[i];
} }
} }
} }

View File

@ -67,6 +67,10 @@ export const generatePassword = async (length: number = 24): Promise<string> =>
return buffer.toString("hex").substring(0, length); return buffer.toString("hex").substring(0, length);
}; };
export const hash = (str: string): string => {
return crypto.createHash("sha256").update(str).digest("hex");
};
export const getMediaMime = (filePath?: string): string => { export const getMediaMime = (filePath?: string): string => {
return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{ return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{
".css": "text/css", ".css": "text/css",