mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-18 21:57:20 +02:00
chore(deps): upgrade dependencies and remove production vulnerabilities
This commit is contained in:
1 parent
9435a99b6c
commit
fd0dd0adbd
29 files changed
+5220
-21129
No files matched your search
@@ -0,0 +1,81 @@
|
||||
/* global fetch */
|
||||
const { expect } = require("chai");
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const { createClient } = require("redis");
|
||||
const { RedisStore: SessionStore } = require("connect-redis");
|
||||
const { RedisStore: RateLimitStore } = require("rate-limit-redis");
|
||||
const { rateLimit } = require("express-rate-limit");
|
||||
const { execFile } = require("node:child_process");
|
||||
const { promisify } = require("node:util");
|
||||
const path = require("node:path");
|
||||
const port = Number(process.env.PERFORMANCE_REDIS_PORT);
|
||||
|
||||
(port ? describe : describe.skip)("upgraded Redis stores and session cleanup", function () {
|
||||
this.timeout(15000);
|
||||
let redis, server, base;
|
||||
const prefix = `dependency-test:${process.pid}:`;
|
||||
before(async function () {
|
||||
redis = createClient({ socket: { host: "127.0.0.1", port, reconnectStrategy: false } });
|
||||
redis.on("error", () => {});
|
||||
await redis.connect();
|
||||
const app = express();
|
||||
app.use(session({ secret: "dependency-test", resave: false, saveUninitialized: false,
|
||||
store: new SessionStore({ client: redis, prefix: `${prefix}session:` }) }));
|
||||
app.get("/session", (req, res) => {
|
||||
req.session.visits = (req.session.visits || 0) + 1;
|
||||
res.json({ visits: req.session.visits });
|
||||
});
|
||||
const limiter = () => rateLimit({ windowMs: 60000, limit: 1, keyGenerator: () => "client",
|
||||
store: new RateLimitStore({ prefix: `${prefix}limit:`, sendCommand: (...args) => redis.sendCommand(args) }) });
|
||||
app.get("/first", limiter(), (_req, res) => res.send("ok"));
|
||||
app.get("/second", limiter(), (_req, res) => res.send("ok"));
|
||||
await new Promise(resolve => { server = app.listen(0, "127.0.0.1", resolve); });
|
||||
base = `http://127.0.0.1:${server.address().port}`;
|
||||
});
|
||||
after(async function () {
|
||||
if (server) {
|
||||
server.closeAllConnections();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
}
|
||||
if (redis?.isOpen) {
|
||||
for await (const keys of redis.scanIterator({ MATCH: `${prefix}*` })) {
|
||||
if (keys.length) await redis.del(keys);
|
||||
}
|
||||
await redis.del([`anoGH_session:${prefix}legacy`, `anoGH_session:${prefix}current`]);
|
||||
await redis.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("persists sessions across HTTP requests", async function () {
|
||||
const first = await fetch(`${base}/session`);
|
||||
expect(await first.json()).to.deep.equal({ visits: 1 });
|
||||
const cookie = first.headers.get("set-cookie").split(";")[0];
|
||||
const second = await fetch(`${base}/session`, { headers: { Cookie: cookie } });
|
||||
expect(await second.json()).to.deep.equal({ visits: 2 });
|
||||
});
|
||||
|
||||
it("shares a request quota between separate Redis store instances", async function () {
|
||||
expect((await fetch(`${base}/first`)).status).to.equal(200);
|
||||
expect((await fetch(`${base}/second`)).status).to.equal(429);
|
||||
});
|
||||
|
||||
it("scans session batches, preserves current sessions and applies legacy cleanup", async function () {
|
||||
const legacy = `anoGH_session:${prefix}legacy`, current = `anoGH_session:${prefix}current`;
|
||||
await redis.set(legacy, JSON.stringify({ passport: { user: { id: "legacy" } } }));
|
||||
await redis.set(current, JSON.stringify({ passport: { user: "current" } }));
|
||||
const run = async args => {
|
||||
const result = await promisify(execFile)(process.execPath,
|
||||
["-r", "ts-node/register", "src/scripts/purge-legacy-sessions.ts", ...args], {
|
||||
cwd: path.resolve(__dirname, ".."), timeout: 10000,
|
||||
env: { ...process.env, NODE_ENV: "test", REDIS_HOSTNAME: "127.0.0.1", REDIS_PORT: String(port) },
|
||||
});
|
||||
return JSON.parse(result.stdout.trim().split("\n").at(-1));
|
||||
};
|
||||
expect(await run([])).to.deep.equal({ found: 1, removed: 0 });
|
||||
expect(await redis.exists(legacy)).to.equal(1);
|
||||
expect(await run(["--apply"])).to.deep.equal({ found: 1, removed: 1 });
|
||||
expect(await redis.exists(legacy)).to.equal(0);
|
||||
expect(await redis.get(current)).to.equal(JSON.stringify({ passport: { user: "current" } }));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/* global fetch */
|
||||
const { expect } = require("chai");
|
||||
const http = require("node:http");
|
||||
const net = require("node:net");
|
||||
const path = require("node:path");
|
||||
const { spawn } = require("node:child_process");
|
||||
const { once } = require("node:events");
|
||||
const { setTimeout, clearTimeout } = require("node:timers");
|
||||
|
||||
describe("development proxy", function () {
|
||||
this.timeout(15000);
|
||||
let upstream, child, base, target;
|
||||
before(async function () {
|
||||
upstream = http.createServer((req, res) => {
|
||||
if (req.url === "/api/headers") {
|
||||
res.setHeader("content-type", "application/json");
|
||||
return res.end(JSON.stringify({ origin: req.headers.origin, referer: req.headers.referer }));
|
||||
}
|
||||
res.setHeader("set-cookie", "test=value; Domain=localhost; Secure; SameSite=None; Path=/");
|
||||
if (req.url === "/api/redirect") {
|
||||
res.writeHead(302, { location: `${target}/api/next` });
|
||||
return res.end();
|
||||
}
|
||||
res.setHeader("content-type", "text/html");
|
||||
res.end(`<a href="${target}/api/next">next</a>`);
|
||||
});
|
||||
await new Promise(resolve => upstream.listen(0, "127.0.0.1", resolve));
|
||||
target = `http://127.0.0.1:${upstream.address().port}`;
|
||||
const socket = net.createServer();
|
||||
await new Promise(resolve => socket.listen(0, "127.0.0.1", resolve));
|
||||
const port = socket.address().port;
|
||||
await new Promise(resolve => socket.close(resolve));
|
||||
base = `http://127.0.0.1:${port}`;
|
||||
child = spawn(process.execPath, ["scripts/dev-proxy.js"], {
|
||||
cwd: path.resolve(__dirname, ".."), env: { ...process.env, UPSTREAM: target, PORT: String(port) },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
let output = "", errors = "";
|
||||
const timer = setTimeout(() => reject(new Error("Development proxy startup timed out")), 10000);
|
||||
child.stderr.on("data", chunk => { errors += chunk; });
|
||||
child.stdout.on("data", chunk => {
|
||||
output += chunk;
|
||||
if (output.includes("dev-proxy")) { clearTimeout(timer); resolve(); }
|
||||
});
|
||||
child.once("exit", code => { clearTimeout(timer); reject(new Error(`Proxy exited with ${code}: ${errors}`)); });
|
||||
});
|
||||
});
|
||||
after(async function () {
|
||||
if (child && child.exitCode === null && child.signalCode === null) {
|
||||
const exited = once(child, "exit");
|
||||
child.kill();
|
||||
await exited;
|
||||
}
|
||||
if (upstream) {
|
||||
upstream.closeAllConnections();
|
||||
await new Promise(resolve => upstream.close(resolve));
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards the request hooks to the upstream", async function () {
|
||||
const response = await fetch(`${base}/api/headers`);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.json()).to.deep.equal({ origin: target, referer: `${target}/api/headers` });
|
||||
});
|
||||
|
||||
it("rewrites HTML, cookies and redirects for localhost", async function () {
|
||||
const response = await fetch(`${base}/api/html`);
|
||||
expect(await response.text()).to.equal('<a href="/api/next">next</a>');
|
||||
const cookie = response.headers.get("set-cookie");
|
||||
expect(cookie).to.include("SameSite=Lax");
|
||||
expect(cookie).not.to.match(/Secure|Domain=/i);
|
||||
const redirect = await fetch(`${base}/api/redirect`, { redirect: "manual" });
|
||||
expect(redirect.status).to.equal(302);
|
||||
expect(redirect.headers.get("location")).to.equal("/api/next");
|
||||
});
|
||||
|
||||
it("serves the local UI with resolved asset names", async function () {
|
||||
const response = await fetch(base);
|
||||
expect(response.status).to.equal(200);
|
||||
const body = await response.text();
|
||||
expect(body).to.include('id="app"');
|
||||
expect(body).not.to.include("__CORE_JS__");
|
||||
expect(body).not.to.include("__VENDOR_JS__");
|
||||
});
|
||||
});
|
||||
@@ -65,13 +65,13 @@ describe("download headers and website revalidation", function () {
|
||||
expect(second.headers["content-security-policy"]).to.include("sandbox");
|
||||
});
|
||||
it("renders Markdown again after a transient failure served the raw fallback", async function () {
|
||||
const marked = require("marked"), render = marked.marked;
|
||||
const marked = require("marked").marked, render = marked.parse;
|
||||
let first;
|
||||
try {
|
||||
marked.marked = () => { throw Error("transient rendering failure"); };
|
||||
marked.parse = () => { throw Error("transient rendering failure"); };
|
||||
first = await fetch("/w/fixture/page.md");
|
||||
expect(first.status).to.equal(200); expect(first.body).to.equal("fixture content");
|
||||
} finally { marked.marked = render; }
|
||||
} finally { marked.parse = render; }
|
||||
const recovered = await fetch("/w/fixture/page.md", "GET", { "If-None-Match": first.headers.etag });
|
||||
expect(recovered.status).to.equal(200); expect(recovered.body).to.include("Anonymized page");
|
||||
expect(recovered.headers.etag).not.to.equal(first.headers.etag);
|
||||
|
||||
@@ -34,7 +34,7 @@ const sanitizeOptions = {
|
||||
* sanitizeHtml(marked(content, opts), sanitizeOptions)
|
||||
*/
|
||||
function renderAndSanitize(markdown) {
|
||||
const raw = marked(markdown, { headerIds: false, mangle: false });
|
||||
const raw = marked(markdown, { async: false });
|
||||
return sanitizeHtml(raw, sanitizeOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ const port = Number(process.env.PERFORMANCE_REDIS_PORT);
|
||||
});
|
||||
beforeEach(async function () {
|
||||
const keys = [];
|
||||
for await (const key of redis.scanIterator({ MATCH: "performance:v1:*" })) keys.push(key);
|
||||
for await (const batch of redis.scanIterator({ MATCH: "performance:v1:*" })) keys.push(...batch);
|
||||
if (keys.length) await redis.del(keys);
|
||||
requestMetrics.drain();
|
||||
});
|
||||
afterEach(async function () { if (stop) { await stop(); stop = null; } requestMetrics.drain(); });
|
||||
after(async function () { await redis.disconnect(); config.REDIS_HOSTNAME = previous.host; config.REDIS_PORT = previous.port; });
|
||||
after(async function () { await redis.close(); config.REDIS_HOSTNAME = previous.host; config.REDIS_PORT = previous.port; });
|
||||
async function start(service) {
|
||||
stop = startPerformanceMonitoring(service, () => ({ running: 0, waiting: 0 }));
|
||||
const deadline = Date.now() + 4000;
|
||||
|
||||
@@ -26,7 +26,7 @@ const { setTimeout: delay } = require("node:timers/promises");
|
||||
redis = createClient({ socket: { host: "127.0.0.1", port: config.REDIS_PORT } });
|
||||
await redis.connect();
|
||||
});
|
||||
after(async () => { await closeCacheRedis(); await redis.quit(); Object.assign(config, previousRedis); await fs.rm(root, { force: true, recursive: true }); });
|
||||
after(async () => { await closeCacheRedis(); await redis.close(); Object.assign(config, previousRedis); await fs.rm(root, { force: true, recursive: true }); });
|
||||
it("downloads and publishes once across two streamer processes and ten callers", async () => {
|
||||
let producers = 0;
|
||||
const children = Array.from({ length: 2 }, () => fork(path.join(__dirname, "fixtures/performance-cache-worker.js"), [], { stdio: ["ignore", "ignore", "ignore", "ipc"], env: { ...process.env,
|
||||
|
||||
@@ -32,8 +32,8 @@ const uri = process.env.PERFORMANCE_MONGO_URI;
|
||||
beforeEach(async () => { await Repo.deleteMany({}); });
|
||||
|
||||
const missing = async file => { try { await fs.access(file); return false; } catch (error) { if (error.code === "ENOENT") return true; throw error; } };
|
||||
async function waitFor(check) {
|
||||
const deadline = Date.now() + 5000;
|
||||
async function waitFor(check, timeoutMs = 5000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (!await check()) { if (Date.now() >= deadline) throw Error("Streamer cleanup did not finish"); await new Promise(resolve => setTimeout(resolve, 20)); }
|
||||
}
|
||||
async function launch(invalidCredentials = false, holdConnect = false) {
|
||||
@@ -78,7 +78,9 @@ const uri = process.env.PERFORMANCE_MONGO_URI;
|
||||
it("connects before serving and rejects a fill published after expiration in the actual streamer entrypoint", async () => {
|
||||
const runtime = await launch(false, true); let pending;
|
||||
try {
|
||||
await waitFor(() => runtime.messages.some(message => message.connectionHeld));
|
||||
// Loading TypeScript and the Redis client in a fresh process can take
|
||||
// longer than the cleanup deadline used after the streamer starts.
|
||||
await waitFor(() => runtime.messages.some(message => message.connectionHeld), 10000);
|
||||
await fetch(runtime.url + "/healthcheck").then(() => { throw Error("Streamer served before connecting"); }, error => expect(error).to.be.instanceOf(Error));
|
||||
runtime.child.send("connect");
|
||||
await runtime.ready;
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("large anonymization resources", function () {
|
||||
});
|
||||
it("bounds an unresponsive Redis command and disconnects its socket", async () => {
|
||||
let disconnected = false, error;
|
||||
const client = { isOpen: true, disconnect: async () => { disconnected = true; } };
|
||||
const client = { isOpen: true, destroy: () => { disconnected = true; } };
|
||||
try { await cacheCommand(new Promise(() => {}), client, 20); } catch (failure) { error = failure; }
|
||||
expect(error.message).to.equal("cache_command_timeout"); expect(disconnected).to.equal(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* global fetch */
|
||||
const { expect } = require("chai");
|
||||
const express = require("express");
|
||||
const { rateLimit } = require("express-rate-limit");
|
||||
require("ts-node/register/transpile-only");
|
||||
const { requestRateLimitKey } = require("../src/server/rate-limit-key");
|
||||
|
||||
describe("rate limiter client addresses", function () {
|
||||
const key = (ip, headers = {}, remoteAddress = "127.0.0.1") =>
|
||||
requestRateLimitKey({ ip, headers, socket: { remoteAddress } });
|
||||
|
||||
it("normalizes IPv4 ports and mapped IPv6 without truncating native IPv6", function () {
|
||||
expect(key("192.0.2.1:1234")).to.equal("192.0.2.1");
|
||||
expect(key("::ffff:192.0.2.1")).to.equal("192.0.2.1");
|
||||
expect(key("2001:db8:abcd:1200::1234")).to.equal("2001:db8:abcd:1200::/56");
|
||||
expect(key("2001:db8:abcd:1300::1234")).not.to.equal(key("2001:db8:abcd:1200::1234"));
|
||||
});
|
||||
|
||||
it("accepts the visitor header only for a Cloudflare edge address", function () {
|
||||
expect(key("192.0.2.1", { "cf-connecting-ip": "198.51.100.1" })).to.equal("192.0.2.1");
|
||||
expect(key("104.16.0.1", { "cf-connecting-ip": " 198.51.100.1 " })).to.equal("198.51.100.1");
|
||||
expect(key("104.16.0.1", { "cf-connecting-ip": " " })).to.equal("104.16.0.1");
|
||||
expect(key(undefined, {}, "::ffff:192.0.2.1")).to.equal("192.0.2.1");
|
||||
});
|
||||
|
||||
it("shares limits across IPv6 addresses within a subnet and ignores forged Cloudflare headers", async function () {
|
||||
const app = express();
|
||||
app.set("trust proxy", "loopback");
|
||||
app.use(rateLimit({ windowMs: 60000, limit: 1, keyGenerator: requestRateLimitKey }));
|
||||
app.get("/", (_req, res) => res.send("ok"));
|
||||
let server;
|
||||
await new Promise(resolve => { server = app.listen(0, "127.0.0.1", resolve); });
|
||||
const request = (ip, visitor) => fetch(`http://127.0.0.1:${server.address().port}/`, {
|
||||
headers: { "x-forwarded-for": ip, "cf-connecting-ip": visitor },
|
||||
});
|
||||
try {
|
||||
expect((await request("2001:db8:abcd:1200::1", "198.51.100.1")).status).to.equal(200);
|
||||
expect((await request("2001:db8:abcd:12ff::2", "198.51.100.2")).status).to.equal(429);
|
||||
expect((await request("2001:db8:abcd:1300::1", "198.51.100.1")).status).to.equal(200);
|
||||
expect((await request("192.0.2.1", "198.51.100.1")).status).to.equal(200);
|
||||
expect((await request("::ffff:192.0.2.1", "198.51.100.2")).status).to.equal(429);
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
}
|
||||
});
|
||||
});
|
||||
+12
-9
@@ -1,9 +1,10 @@
|
||||
const { expect } = require("chai");
|
||||
const { JSDOM, VirtualConsole, ResourceLoader } = require("jsdom");
|
||||
const { JSDOM, VirtualConsole, requestInterceptor } = require("jsdom");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { URL } = require("node:url");
|
||||
const { setTimeout: delay } = require("node:timers/promises");
|
||||
const { Response } = globalThis;
|
||||
|
||||
const publicDir = path.join(__dirname, "../public");
|
||||
const bundles = ["core.min.js", "vendor.min.js"].map(name => fs.readFileSync(path.join(publicDir, "script", name), "utf8"));
|
||||
@@ -12,19 +13,21 @@ async function browser(route = "/", overrides = {}, storage = {}) {
|
||||
const errors = [], requests = [], assets = [];
|
||||
const virtualConsole = new VirtualConsole();
|
||||
virtualConsole.on("jsdomError", error => {
|
||||
if (!error.message.includes("navigation (except hash changes)")) errors.push(error.message);
|
||||
if (error.type === "not-implemented" && error.message === "Not implemented: navigation to another Document") return;
|
||||
errors.push(error.message);
|
||||
});
|
||||
virtualConsole.on("error", error => errors.push(error?.message || String(error)));
|
||||
const dom = new JSDOM('<!doctype html><html><head></head><body><div id="app"></div></body></html>', {
|
||||
resources: new class extends ResourceLoader {
|
||||
fetch(url) {
|
||||
resources: {
|
||||
interceptors: [requestInterceptor(request => {
|
||||
const url = request.url;
|
||||
assets.push(new URL(url).pathname);
|
||||
if (/\/pdf\.[a-f0-9]+\.min\.js$/.test(new URL(url).pathname) && dom.window.pdfjsLib) return Promise.resolve(Buffer.from(""));
|
||||
if (/\/pdf\.[a-f0-9]+\.min\.js$/.test(new URL(url).pathname) && dom.window.pdfjsLib) return new Response("");
|
||||
const pathname = new URL(url).pathname.replace(/\.[a-f0-9]{10}\.min\.js$/, ".min.js");
|
||||
if (pathname.startsWith("/script/")) return Promise.resolve(fs.readFileSync(path.join(publicDir, pathname)));
|
||||
return null;
|
||||
}
|
||||
}(),
|
||||
if (pathname.startsWith("/script/")) return new Response(fs.readFileSync(path.join(publicDir, pathname)), { headers: { "content-type": "application/javascript" } });
|
||||
return new Response(null, { status: 204 });
|
||||
})],
|
||||
},
|
||||
url: "http://localhost" + route, runScripts: "dangerously", pretendToBeVisual: true, virtualConsole,
|
||||
});
|
||||
const window = dom.window;
|
||||
|
||||
Reference in new issue
Block a user