fix(security): replace nookies with patched cookie utility
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { parseCookies, setCookie } from "nookies";
|
||||
import { parseCookies, setCookie } from "../lib/cookies";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const COOKIE_NAME = "pedwAiPolicyNoticeHidden";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import React, { useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { updateAccount } from "../../actions/services/accountService";
|
||||
import { setCookie } from "nookies";
|
||||
import { setCookie } from "../../lib/cookies";
|
||||
|
||||
const PersonalDetailsComplete = (props) => {
|
||||
const {
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from "../../store/watchedCases/action";
|
||||
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { destroyCookie, parseCookies, setCookie } from "nookies";
|
||||
import { destroyCookie, parseCookies, setCookie } from "../../lib/cookies";
|
||||
import { getFormCollectionByID, getDetailsProxy, hasOwn } from "../utils";
|
||||
import {
|
||||
getLivePublishedEvent,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { signOut } from "next-auth/react";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { destroyCookie, parseCookies, setCookie } from "nookies";
|
||||
import { destroyCookie, parseCookies, setCookie } from "../../lib/cookies";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
|
||||
const WatchModal = (props) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { reduxForm } from "redux-form";
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { destroyCookie, setCookie } from "nookies";
|
||||
import { destroyCookie, setCookie } from "../lib/cookies";
|
||||
import { connect } from "react-redux";
|
||||
import Banner from "../components/banner";
|
||||
import { setLogout } from "../store/accountDetails/action";
|
||||
|
||||
@@ -2,7 +2,7 @@ import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import { setLoggedInUserId } from "../../store/accountDetails/action";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import CaseComment from "./casecomment";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
deleteAwaitingSubmissionsFromBlob,
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
deleteAwaitingSubmissions,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
deleteAwaitingSubmissions,
|
||||
|
||||
@@ -3,7 +3,7 @@ import _ from "lodash";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { connect } from "react-redux";
|
||||
import transLookup from "../../data/lookuptranslations.json";
|
||||
import { setCurrentReference } from "../../store/currentView/action";
|
||||
|
||||
import { parseCookies, setCookie } from "nookies";
|
||||
import { parseCookies, setCookie } from "../../lib/cookies";
|
||||
import { useEffect } from "react";
|
||||
import { setLoggedInUserId } from "../../store/accountDetails/action";
|
||||
import {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "../../actions/services/searchService";
|
||||
|
||||
import GoogleMapReact from "google-map-react";
|
||||
import { parseCookies } from "nookies";
|
||||
import { parseCookies } from "../../lib/cookies";
|
||||
import OSPoint from "ospoint";
|
||||
import {
|
||||
createWatchedCases,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
setCurrentReference
|
||||
} from "../../store/currentView/action";
|
||||
|
||||
import { parseCookies, setCookie } from "nookies";
|
||||
import { parseCookies, setCookie } from "../../lib/cookies";
|
||||
import { useEffect } from "react";
|
||||
import { setLoggedInUserId } from "../../store/accountDetails/action";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { destroyCookie } from "nookies";
|
||||
import { destroyCookie } from "../cookies";
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
const AUTH_COOKIE_CANDIDATES = [
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import * as cookie from "cookie";
|
||||
import setCookieParser from "set-cookie-parser";
|
||||
|
||||
const isBrowser = () => typeof window !== "undefined";
|
||||
|
||||
const createCookie = (name, value, options) => {
|
||||
let sameSite = options.sameSite;
|
||||
|
||||
if (sameSite === true) {
|
||||
sameSite = "strict";
|
||||
}
|
||||
|
||||
if (sameSite === undefined || sameSite === false) {
|
||||
sameSite = "lax";
|
||||
}
|
||||
|
||||
const cookieToSet = { ...options, sameSite };
|
||||
delete cookieToSet.encode;
|
||||
|
||||
return { name, value, ...cookieToSet };
|
||||
};
|
||||
|
||||
const hasSameProperties = (firstCookie, secondCookie) => {
|
||||
const firstProperties = Object.getOwnPropertyNames(firstCookie);
|
||||
const secondProperties = Object.getOwnPropertyNames(secondCookie);
|
||||
|
||||
if (firstProperties.length !== secondProperties.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return firstProperties.every(
|
||||
(propertyName) =>
|
||||
firstCookie[propertyName] === secondCookie[propertyName]
|
||||
);
|
||||
};
|
||||
|
||||
const areCookiesEqual = (firstCookie, secondCookie) => {
|
||||
let sameSiteMatches = firstCookie.sameSite === secondCookie.sameSite;
|
||||
|
||||
if (
|
||||
typeof firstCookie.sameSite === "string" &&
|
||||
typeof secondCookie.sameSite === "string"
|
||||
) {
|
||||
sameSiteMatches =
|
||||
firstCookie.sameSite.toLowerCase() ===
|
||||
secondCookie.sameSite.toLowerCase();
|
||||
}
|
||||
|
||||
return (
|
||||
hasSameProperties(
|
||||
{ ...firstCookie, sameSite: undefined },
|
||||
{ ...secondCookie, sameSite: undefined }
|
||||
) && sameSiteMatches
|
||||
);
|
||||
};
|
||||
|
||||
export const parseCookies = (ctx, options) => {
|
||||
if (ctx?.req?.headers?.cookie) {
|
||||
return cookie.parse(ctx.req.headers.cookie, options);
|
||||
}
|
||||
|
||||
if (isBrowser()) {
|
||||
return cookie.parse(document.cookie, options);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const setCookie = (ctx, name, value, options = {}) => {
|
||||
if (ctx?.res?.getHeader && ctx.res.setHeader) {
|
||||
if (ctx?.res?.finished) {
|
||||
console.warn(`Not setting "${name}" cookie. Response has finished.`);
|
||||
console.warn("You should set cookie before res.send()");
|
||||
return {};
|
||||
}
|
||||
|
||||
let cookies = ctx.res.getHeader("Set-Cookie") || [];
|
||||
|
||||
if (typeof cookies === "string") {
|
||||
cookies = [cookies];
|
||||
}
|
||||
|
||||
if (typeof cookies === "number") {
|
||||
cookies = [];
|
||||
}
|
||||
|
||||
const parsedCookies = setCookieParser.parse(cookies, {
|
||||
decodeValues: false
|
||||
});
|
||||
const newCookie = createCookie(name, value, options);
|
||||
const cookiesToSet = [];
|
||||
|
||||
parsedCookies.forEach((parsedCookie) => {
|
||||
if (!areCookiesEqual(parsedCookie, newCookie)) {
|
||||
cookiesToSet.push(
|
||||
cookie.serialize(parsedCookie.name, parsedCookie.value, {
|
||||
encode: (cookieValue) => cookieValue,
|
||||
...parsedCookie
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cookiesToSet.push(cookie.serialize(name, value, options));
|
||||
ctx.res.setHeader("Set-Cookie", cookiesToSet);
|
||||
}
|
||||
|
||||
if (isBrowser()) {
|
||||
if (options?.httpOnly) {
|
||||
throw new Error("Can not set a httpOnly cookie in the browser.");
|
||||
}
|
||||
|
||||
document.cookie = cookie.serialize(name, value, options);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const destroyCookie = (ctx, name, options) =>
|
||||
setCookie(ctx, name, "", { ...(options || {}), maxAge: -1 });
|
||||
Generated
+13
-34
@@ -21,6 +21,7 @@
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"applicationinsights": "^2.9.1",
|
||||
"axios": "^1.21.0",
|
||||
"cookie": "0.7.2",
|
||||
"cookie-cutter": "^0.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^2.30.0",
|
||||
@@ -46,7 +47,6 @@
|
||||
"next-translate": "^2.6.2",
|
||||
"next-translate-plugin": "^2.6.2",
|
||||
"nodemailer": "7.0.13",
|
||||
"nookies": "^2.5.2",
|
||||
"notifications-node-client": "^7.0.6",
|
||||
"openapi-types": "^12.1.3",
|
||||
"ospoint": "^0.2.1",
|
||||
@@ -73,6 +73,7 @@
|
||||
"redux-thunk": "^2.4.2",
|
||||
"sass": "1.69",
|
||||
"sass-loader": "13.3.2",
|
||||
"set-cookie-parser": "2.7.0",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.3.2",
|
||||
"uuid": "9.0.0",
|
||||
@@ -3716,6 +3717,15 @@
|
||||
"emitter-listener": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-cutter": {
|
||||
"version": "0.2.0",
|
||||
"license": "MIT"
|
||||
@@ -4919,14 +4929,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -6940,14 +6942,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/preact": {
|
||||
"version": "10.24.0",
|
||||
"license": "MIT",
|
||||
@@ -7101,23 +7095,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nookies": {
|
||||
"version": "2.5.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^0.4.1",
|
||||
"set-cookie-parser": "^2.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nookies/node_modules/cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
@@ -8629,6 +8606,8 @@
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz",
|
||||
"integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
|
||||
+2
-1
@@ -43,6 +43,7 @@
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"applicationinsights": "^2.9.1",
|
||||
"axios": "^1.21.0",
|
||||
"cookie": "0.7.2",
|
||||
"cookie-cutter": "^0.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^2.30.0",
|
||||
@@ -68,7 +69,6 @@
|
||||
"next-translate": "^2.6.2",
|
||||
"next-translate-plugin": "^2.6.2",
|
||||
"nodemailer": "7.0.13",
|
||||
"nookies": "^2.5.2",
|
||||
"notifications-node-client": "^7.0.6",
|
||||
"openapi-types": "^12.1.3",
|
||||
"ospoint": "^0.2.1",
|
||||
@@ -95,6 +95,7 @@
|
||||
"redux-thunk": "^2.4.2",
|
||||
"sass": "1.69",
|
||||
"sass-loader": "13.3.2",
|
||||
"set-cookie-parser": "2.7.0",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.3.2",
|
||||
"uuid": "9.0.0",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import Document, { Html, Head, Main, NextScript } from "next/document";
|
||||
import SkipLink from "../components/skiplink";
|
||||
import { Tracking, TrackingNoScript } from "../components/tracking";
|
||||
//import { nanoid } from "nanoid";
|
||||
import { setCookie } from "nookies";
|
||||
import { setCookie } from "../lib/cookies";
|
||||
import Script from "next/script";
|
||||
|
||||
class MyDocument extends Document {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import CookieBanner from "../components/cookieBanner";
|
||||
import Footer from "../components/footer";
|
||||
import Header from "../components/header";
|
||||
import { parseCookies, setCookie, destroyCookie } from "nookies";
|
||||
import { parseCookies, setCookie, destroyCookie } from "../lib/cookies";
|
||||
import { useSession, signIn, signOut } from "next-auth/react";
|
||||
|
||||
function Error({ statusCode }, props) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { reduxForm, formValueSelector } from "redux-form";
|
||||
import PersonalDetails from "../../components/account/personaldetails";
|
||||
import { setAccountDetails } from "../../store/accountDetails/action";
|
||||
import { getPersonalAccount } from "../../actions/services/accountService";
|
||||
import { setCookie } from "nookies";
|
||||
import { setCookie } from "../../lib/cookies";
|
||||
import { getSession, useSession } from "next-auth/react";
|
||||
import NoSessionWarning from "../../components/nosession";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import CookieBanner from "../../components/cookieBanner";
|
||||
import Footer from "../../components/footer";
|
||||
import Header from "../../components/header";
|
||||
import ServiceBanner from "../../components/servicebanner";
|
||||
import { setCookie } from "nookies";
|
||||
import { setCookie } from "../../lib/cookies";
|
||||
|
||||
const SignIn = (props) => {
|
||||
let { t, lang } = useTranslation();
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { signOut, useSession } from "next-auth/react";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Head from "next/head";
|
||||
import Router from "next/router";
|
||||
import { destroyCookie } from "nookies";
|
||||
import { destroyCookie } from "../lib/cookies";
|
||||
import CookieBanner from "../components/cookieBanner";
|
||||
import Footer from "../components/footer";
|
||||
import Header from "../components/header";
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { getSession } from "next-auth/react";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { setCookie } from "nookies";
|
||||
import { setCookie } from "../lib/cookies";
|
||||
import { connect } from "react-redux";
|
||||
import { hashString } from "../actions/core/hash";
|
||||
import { getIP } from "../actions/core/logger";
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
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: "/" })),
|
||||
{}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user