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:
parent
a1d6bcb8e5
commit
2018024810
|
@ -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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue