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 { Protocol } from "vs/server/src/node/protocol";
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 { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
@ -98,7 +98,11 @@ export interface Response {
}
export interface LoginPayload {
password?: string[] | string;
password?: string;
}
export interface AuthPayload {
key?: string[];
}
export class HttpError extends Error {
@ -137,6 +141,7 @@ export abstract class Server {
host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
...options,
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
password: options.password ? hash(options.password) : undefined,
};
this.protocol = this.options.cert ? "https" : "http";
if (this.protocol === "https") {
@ -357,11 +362,11 @@ export abstract class Server {
}
private async tryLogin(request: http.IncomingMessage): Promise<Response> {
const redirect = (password?: string | string[] | true) => {
const redirect = (password: string | true) => {
return {
redirect: "/",
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") {
const data = await this.getData<LoginPayload>(request);
if (this.authenticate(request, data)) {
return redirect(data.password);
const password = this.authenticate(request, {
key: typeof data.password === "string" ? [hash(data.password)] : undefined,
});
if (password) {
return redirect(password);
}
console.error("Failed login attempt", JSON.stringify({
xForwardedFor: request.headers["x-forwarded-for"],
@ -432,19 +440,18 @@ export abstract class Server {
: Promise.resolve({} as T);
}
private authenticate(request: http.IncomingMessage, payload?: LoginPayload): string | boolean {
if (this.options.auth !== "password") {
private authenticate(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
if (this.options.auth === "none") {
return true;
}
const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
if (typeof payload === "undefined") {
payload = this.parseCookies<LoginPayload>(request);
payload = this.parseCookies<AuthPayload>(request);
}
if (this.options.password && payload.password) {
const toTest = Array.isArray(payload.password) ? payload.password : [payload.password];
for (let i = 0; i < toTest.length; ++i) {
if (safeCompare(toTest[i], this.options.password)) {
return toTest[i];
if (this.options.password && payload.key) {
for (let i = 0; i < payload.key.length; ++i) {
if (safeCompare(payload.key[i], this.options.password)) {
return payload.key[i];
}
}
}

View File

@ -67,6 +67,10 @@ export const generatePassword = async (length: number = 24): Promise<string> =>
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 => {
return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{
".css": "text/css",