Files
pedwfrontend/tests/security/cookie-compatibility.test.cjs
T

331 lines
10 KiB
JavaScript

const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");
const rootDir = path.resolve(__dirname, "..", "..");
const normalize = (value) => JSON.parse(JSON.stringify(value));
const loadLocalCookieCompatibility = () => {
const filePath = path.join(rootDir, "lib", "cookies.js");
let source = fs.readFileSync(filePath, "utf8");
source = source.replace(
'import * as cookie from "cookie";',
'const cookie = require("cookie");'
);
source = source.replace(
'import setCookieParser from "set-cookie-parser";',
'const setCookieParser = require("set-cookie-parser");'
);
source = source.replace(/export const\s+/g, "const ");
source += "\nmodule.exports = { parseCookies, setCookie, destroyCookie };\n";
const context = {
module: { exports: {} },
exports: {},
require,
console,
document: global.document,
window: global.window
};
vm.runInNewContext(source, context, { filename: filePath });
return context.module.exports;
};
const loadCookieImplementation = () => {
return loadLocalCookieCompatibility();
};
const withBrowserDocument = async (initialCookies, callback) => {
const previousDocument = global.document;
const previousWindow = global.window;
const cookieJar = new Map(Object.entries(initialCookies));
const writes = [];
const documentMock = {};
Object.defineProperty(documentMock, "cookie", {
configurable: true,
get() {
return Array.from(cookieJar.entries())
.map(([name, value]) => `${name}=${value}`)
.join("; ");
},
set(serializedCookie) {
writes.push(serializedCookie);
const [nameValue, ...attributes] = serializedCookie.split(";");
const separatorIndex = nameValue.indexOf("=");
const name = nameValue.slice(0, separatorIndex);
const value = nameValue.slice(separatorIndex + 1);
const maxAge = attributes
.map((attribute) => attribute.trim())
.find((attribute) =>
attribute.toLowerCase().startsWith("max-age=")
);
if (maxAge && Number(maxAge.split("=")[1]) < 0) {
cookieJar.delete(name);
return;
}
cookieJar.set(name, value);
}
});
global.window = {};
global.document = documentMock;
try {
await callback({ cookieJar, writes });
} finally {
if (previousWindow === undefined) {
delete global.window;
} else {
global.window = previousWindow;
}
if (previousDocument === undefined) {
delete global.document;
} else {
global.document = previousDocument;
}
}
};
const createResponse = (initialHeader) => {
let setCookieHeader = initialHeader;
const calls = [];
return {
response: {
finished: false,
getHeader(name) {
assert.strictEqual(name, "Set-Cookie");
return setCookieHeader;
},
setHeader(name, value) {
calls.push({ name, value });
setCookieHeader = value;
}
},
calls,
getSetCookieHeader: () => setCookieHeader
};
};
test("browser parsing preserves characterized nookies behavior", async () => {
await withBrowserDocument(
{
first: "one",
encoded: "two%20words",
equals: "a%3Db",
spaces: "hello%20world",
special: "%21%40%23%24%25%5E%26*%28%29",
malformed: "%E0%A4%A"
},
() => {
const { parseCookies } = loadCookieImplementation();
assert.deepStrictEqual(parseCookies(), {
first: "one",
encoded: "two words",
equals: "a=b",
spaces: "hello world",
special: "!@#$%^&*()",
malformed: "%E0%A4%A"
});
}
);
await withBrowserDocument({}, () => {
const { parseCookies } = loadCookieImplementation();
assert.deepStrictEqual(parseCookies(), {});
});
});
test("parseCookies returns an empty object without a browser document", () => {
const { parseCookies } = loadCookieImplementation();
assert.deepStrictEqual(normalize(parseCookies()), {});
assert.deepStrictEqual(normalize(parseCookies(null)), {});
assert.deepStrictEqual(normalize(parseCookies({})), {});
});
test("browser writes preserve encoding and supported options", async () => {
await withBrowserDocument({}, ({ writes }) => {
const { setCookie } = loadCookieImplementation();
const expires = new Date("2030-01-02T03:04:05.000Z");
assert.deepStrictEqual(
normalize(setCookie(null, "basic", "value")),
{}
);
setCookie(null, "pathCookie", "value", { path: "/" });
setCookie(null, "expiresCookie", "value", { expires });
setCookie(null, "maxAgeCookie", "value", { maxAge: 60 });
setCookie(null, "encodedCookie", "hello world=a&b");
assert.deepStrictEqual(writes, [
"basic=value",
"pathCookie=value; Path=/",
"expiresCookie=value; Expires=Wed, 02 Jan 2030 03:04:05 GMT",
"maxAgeCookie=value; Max-Age=60",
"encodedCookie=hello%20world%3Da%26b"
]);
});
});
test("browser writes reject httpOnly cookies", async () => {
await withBrowserDocument({}, () => {
const { setCookie } = loadCookieImplementation();
assert.throws(
() => setCookie(null, "protected", "value", { httpOnly: true }),
/Can not set a httpOnly cookie in the browser\./
);
});
});
test("browser deletion preserves path and uses an empty value with negative maxAge", async () => {
await withBrowserDocument(
{
pedwWatchCase: "case-1",
pinsUser: "contact-1",
pedw_locale: "cy"
},
({ cookieJar, writes }) => {
const { destroyCookie } = loadCookieImplementation();
for (const name of ["pedwWatchCase", "pinsUser", "pedw_locale"]) {
assert.deepStrictEqual(
normalize(destroyCookie(null, name, { path: "/" })),
{}
);
}
assert.deepStrictEqual(writes, [
"pedwWatchCase=; Max-Age=-1; Path=/",
"pinsUser=; Max-Age=-1; Path=/",
"pedw_locale=; Max-Age=-1; Path=/"
]);
assert.strictEqual(cookieJar.has("pedwWatchCase"), false);
assert.strictEqual(cookieJar.has("pinsUser"), false);
assert.strictEqual(cookieJar.has("pedw_locale"), false);
}
);
});
test("server parsing uses ctx.req.headers.cookie", () => {
const { parseCookies } = loadCookieImplementation();
assert.deepStrictEqual(
parseCookies({
req: {
headers: {
cookie: "first=one; encoded=two%20words; equals=a%3Db"
}
}
}),
{
first: "one",
encoded: "two words",
equals: "a=b"
}
);
});
test("server writes a Set-Cookie header and preserves unrelated cookies", () => {
const { setCookie } = loadCookieImplementation();
const responseState = createResponse([
"unrelated=keep; Path=/; SameSite=Lax",
"other=value; HttpOnly; Path=/; SameSite=Strict"
]);
assert.deepStrictEqual(
normalize(
setCookie(
{ res: responseState.response },
"target",
"new value",
{ path: "/" }
)
),
{}
);
assert.deepStrictEqual(normalize(responseState.getSetCookieHeader()), [
"unrelated=keep; Path=/; SameSite=Lax",
"other=value; Path=/; HttpOnly; SameSite=Strict",
"target=new%20value; Path=/"
]);
assert.strictEqual(responseState.calls.length, 1);
});
test("server deduplicates an exactly matching cookie and keeps cookies with changed values or paths", () => {
const { setCookie } = loadCookieImplementation();
const responseState = createResponse([
"target=new; Path=/; SameSite=Lax",
"target=old; Path=/; SameSite=Lax",
"target=keep-a; Path=/a; SameSite=Lax",
"target=keep-b; Path=/b; SameSite=Lax"
]);
setCookie({ res: responseState.response }, "target", "new", {
path: "/",
sameSite: "lax"
});
assert.deepStrictEqual(normalize(responseState.getSetCookieHeader()), [
"target=old; Path=/; SameSite=Lax",
"target=keep-a; Path=/a; SameSite=Lax",
"target=keep-b; Path=/b; SameSite=Lax",
"target=new; Path=/; SameSite=Lax"
]);
});
test("server does not write after the response has finished", () => {
const { setCookie } = loadCookieImplementation();
const responseState = createResponse();
responseState.response.finished = true;
const warnings = [];
const originalWarn = console.warn;
console.warn = (message) => warnings.push(message);
try {
assert.deepStrictEqual(
normalize(
setCookie(
{ res: responseState.response },
"target",
"value"
)
),
{}
);
} finally {
console.warn = originalWarn;
}
assert.deepStrictEqual(responseState.calls, []);
assert.deepStrictEqual(warnings, [
'Not setting "target" cookie. Response has finished.',
"You should set cookie before res.send()"
]);
});
test("server no-op behavior is preserved for null, empty, and undefined contexts", () => {
const { setCookie, destroyCookie } = loadCookieImplementation();
assert.deepStrictEqual(normalize(setCookie(null, "target", "value")), {});
assert.deepStrictEqual(normalize(setCookie({}, "target", "value")), {});
assert.deepStrictEqual(
normalize(setCookie(undefined, "target", "value")),
{}
);
assert.deepStrictEqual(
normalize(destroyCookie({}, "pedwWatchCase", { path: "/" })),
{}
);
});