chore(deps): upgrade dependencies and remove production vulnerabilities

This commit is contained in:
Thomas Durieux committed 2026-10-08 10:21:00 +00:00
1 parent 9435a99b6c
commit fd0dd0adbd
29 files changed
+5220 -21129

No files matched your search

+2 -2
View File
@@ -155,12 +155,12 @@ function ensureRedisGateClient(): Promise<RedisClientType | null> {
socket: {
host: config.REDIS_HOSTNAME,
port: config.REDIS_PORT,
reconnectStrategy: () => false as any,
reconnectStrategy: false,
},
}) as RedisClientType;
c.on("error", () => {
redisGateDisabled = true;
c.disconnect().catch(() => {});
if (c.isOpen) c.destroy();
redisGateReady = null;
});
await c.connect();
+3 -3
View File
@@ -13,7 +13,7 @@ export async function cacheCommand<T>(command: Promise<T>, client: ReturnType<ty
timer = setTimeout(() => {
retryAt = Date.now() + 30_000;
if (connection === client) connection = undefined;
if (client.isOpen) void client.disconnect().catch(() => {});
if (client.isOpen) client.destroy();
reject(new Error("cache_command_timeout"));
}, timeoutMs);
})]);
@@ -30,7 +30,7 @@ export async function cacheRedis() {
} });
client.on("error", () => {});
try { await cacheCommand(client.connect(), client, 500); connection = client; return client; }
catch { retryAt = Date.now() + 30_000; if (client.isOpen) await client.disconnect().catch(() => {}); return undefined; }
catch { retryAt = Date.now() + 30_000; if (client.isOpen) client.destroy(); return undefined; }
finally { connecting = undefined; }
})();
return connecting;
@@ -79,5 +79,5 @@ export async function closeCacheRedis() {
const current = connection;
connection = undefined;
retryAt = 0;
if (current?.isOpen) await current.disconnect();
if (current?.isOpen) current.destroy();
}
+1 -1
View File
@@ -74,7 +74,7 @@ function getRedis(): RedisClientType | null {
const c = redisClient;
redisClient = null;
if (c?.isOpen) {
c.disconnect().catch(() => {});
c.destroy();
}
});
redisClient.connect().catch(() => {
+6 -3
View File
@@ -30,7 +30,10 @@ async function redisDeadline<T>(work: Promise<T>, redis: RedisClientType): Promi
return await Promise.race([work, new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new Error("performance_monitoring_timeout"));
if (redis.isOpen) void redis.disconnect().then(() => client === redis ? redis.connect() : undefined).catch(() => {});
if (redis.isOpen) {
redis.destroy();
if (client === redis) void redis.connect().catch(() => {});
}
}, 2000); timer.unref();
})]);
} finally { clearTimeout(timer); }
@@ -75,7 +78,7 @@ export async function socketSnapshot() {
export function startPerformanceMonitoring(service: Service, workers: () => unknown = () => null) {
if (stop) return stop;
const redis = createClient({ disableOfflineQueue: true, socket: { host: config.REDIS_HOSTNAME,
port: config.REDIS_PORT, connectTimeout: 2000, reconnectStrategy: retries => Math.min(5000, 250 * (retries + 1)) } }) as RedisClientType;
port: config.REDIS_PORT, connectTimeout: 2000, reconnectStrategy: (retries: number) => Math.min(5000, 250 * (retries + 1)) } }) as RedisClientType;
client = redis;
redis.on("error", () => {});
void redis.connect().catch(() => {});
@@ -135,7 +138,7 @@ export function startPerformanceMonitoring(service: Service, workers: () => unkn
void flush();
stop = async () => {
stopped = true; clearInterval(timer); loop.disable();
if (redis.isOpen) await redis.disconnect();
if (redis.isOpen) redis.destroy();
client = undefined; stop = undefined; flushNow = undefined; reports.clear();
};
return stop;
+2 -2
View File
@@ -15,12 +15,12 @@ function getClient(): RedisClientType | null {
socket: {
host: config.REDIS_HOSTNAME,
port: config.REDIS_PORT,
reconnectStrategy: () => false as any,
reconnectStrategy: false,
},
}) as RedisClientType;
client.on("error", () => {
disabled = true;
client?.disconnect().catch(() => {});
if (client?.isOpen) client.destroy();
client = null;
});
client.connect().catch(() => {
+19 -17
View File
@@ -11,26 +11,28 @@ async function main() {
await client.connect();
let found = 0;
let removed = 0;
for await (const key of client.scanIterator({ MATCH: "anoGH_session:*", COUNT: 100 })) {
const raw = await client.get(key);
if (!raw) continue;
let legacy = false;
try {
const value = JSON.parse(raw);
legacy = !!value.passport?.user && typeof value.passport.user !== "string";
} catch { legacy = true; }
if (!legacy) continue;
found++;
if (args.includes("--apply")) {
// Do not remove a session replaced since the scan read it.
removed += Number(await client.eval(
'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end',
{ keys: [key], arguments: [raw] }
));
for await (const keys of client.scanIterator({ MATCH: "anoGH_session:*", COUNT: 100 })) {
for (const key of keys) {
const raw = await client.get(key);
if (!raw) continue;
let legacy = false;
try {
const value = JSON.parse(raw);
legacy = !!value.passport?.user && typeof value.passport.user !== "string";
} catch { legacy = true; }
if (!legacy) continue;
found++;
if (args.includes("--apply")) {
// Do not remove a session replaced since the scan read it.
removed += Number(await client.eval(
'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end',
{ keys: [key], arguments: [raw] }
));
}
}
}
process.stdout.write(JSON.stringify({ found, removed }) + "\n");
} finally { if (client.isOpen) await client.quit(); }
} finally { if (client.isOpen) await client.close(); }
}
main().catch(() => {
process.stderr.write("Legacy session cleanup failed; check Redis configuration and connectivity.\n");
+7 -37
View File
@@ -34,7 +34,8 @@ import {
} from "./dailyStatsSnapshot";
import { getUser } from "./routes/route-utils";
import config from "../config";
import { resolveTrustProxy, isCloudflareIP } from "./trustProxy";
import { resolveTrustProxy } from "./trustProxy";
import { requestRateLimitKey } from "./rate-limit-key";
import { createLogger, serializeError } from "../core/logger";
import { monitorRequests } from "../core/request-monitoring";
import { startPerformanceMonitoring } from "../core/performance-monitoring";
@@ -156,37 +157,6 @@ export default async function start() {
await redisClient.connect();
function keyGenerator(
request: express.Request,
_response: express.Response
): string {
// Use request.ip, which Express resolves from X-Forwarded-For honouring
// the configured "trust proxy" setting. Do NOT key off the
// cf-connecting-ip header unconditionally: when the server isn't actually
// behind Cloudflare a client can set that header to an arbitrary value
// per request and trivially bypass the rate limiter (CWE-290).
let ip = request.ip;
if (!ip && request.socket.remoteAddress) {
logger.warn("request.ip is missing");
ip = request.socket.remoteAddress;
}
// If resolution stopped at a Cloudflare edge address (X-Forwarded-For no
// longer contains the visitor, e.g. after a Cloudflare-side change), fall
// back to cf-connecting-ip. This is safe: request.ip can only be a
// Cloudflare address when every hop up to it is trusted, i.e. the request
// genuinely traversed Cloudflare. A direct client forging the header is
// keyed by its own untrusted address instead, so the CWE-290 bypass above
// does not apply.
if (ip && isCloudflareIP(ip)) {
const cfConnectingIP = request.headers["cf-connecting-ip"];
if (typeof cfConnectingIP === "string" && cfConnectingIP) {
ip = cfConnectingIP.trim();
}
}
// remove port number from IPv4 addresses
return (ip || "").replace(/:\d+[^:]*$/, "");
}
const rate = rateLimit({
store: new RedisStore({
sendCommand: (...args: string[]) => redisClient.sendCommand(args),
@@ -201,7 +171,7 @@ export default async function start() {
}
return false;
},
max: async (request: express.Request, _response: express.Response) => {
limit: async (request: express.Request, _response: express.Response) => {
try {
const user = await getUser(request);
if (user) return config.RATE_LIMIT;
@@ -211,8 +181,8 @@ export default async function start() {
// if not logged in, limit to half the rate
return config.RATE_LIMIT / 2;
},
keyGenerator,
standardHeaders: true,
keyGenerator: requestRateLimitKey,
standardHeaders: "draft-6",
legacyHeaders: false,
message: (_request: express.Request, _response: express.Response) => {
return `You can only make ${config.RATE_LIMIT} requests every 15min. Please try again later.`;
@@ -223,14 +193,14 @@ export default async function start() {
delayAfter: 50,
delayMs: () => 150,
maxDelayMs: 5000,
keyGenerator,
keyGenerator: requestRateLimitKey,
});
const webViewSpeedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 200,
delayMs: () => 150,
maxDelayMs: 5000,
keyGenerator,
keyGenerator: requestRateLimitKey,
});
app.use("/github", rate, speedLimiter, githubAppRouter);
+16
View File
@@ -0,0 +1,16 @@
import { Request } from "express";
import { ipKeyGenerator } from "express-rate-limit";
import { isCloudflareIP } from "./trustProxy";
export function requestRateLimitKey(request: Request): string {
// Express resolves request.ip using the configured trusted proxies.
let ip = request.ip || request.socket.remoteAddress || "";
// Only accept Cloudflare's visitor header when resolution reached its edge.
if (isCloudflareIP(ip)) {
const visitor = request.headers["cf-connecting-ip"];
if (typeof visitor === "string" && visitor.trim()) ip = visitor.trim();
}
// Some proxies append a port to IPv4 addresses. Do not truncate IPv6.
ip = ip.replace(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/, "$1");
return ipKeyGenerator(ip);
}
+3 -3
View File
@@ -600,15 +600,15 @@ router.delete("/errors", async (req, res) => {
// SCAN the hourly counter keys and del them along with the list and
// dropped counter so the admin page comes back to a clean slate.
const hourlyKeys: string[] = [];
let cursor = 0;
let cursor = "0";
do {
const reply = await client.scan(cursor, {
MATCH: `${ERROR_LOG_HOURLY_PREFIX}*`,
COUNT: 100,
});
cursor = Number(reply.cursor);
cursor = reply.cursor;
for (const k of reply.keys) hourlyKeys.push(k);
} while (cursor !== 0);
} while (cursor !== "0");
const pipe = client.multi();
pipe.del(ERROR_LOG_KEY);
pipe.del(ERROR_LOG_DROPPED_KEY);
+1 -2
View File
@@ -1,7 +1,7 @@
import { createClient } from "redis";
import * as passport from "passport";
import * as session from "express-session";
import RedisStore from "connect-redis";
import { RedisStore } from "connect-redis";
import * as OAuth2Strategy from "passport-oauth2";
import { Profile, Strategy } from "passport-github2";
import * as express from "express";
@@ -166,7 +166,6 @@ passport.deserializeUser(async (id: string, done) => {
export function initSession() {
const redisClient = createClient({
legacyMode: false,
socket: {
port: config.REDIS_PORT,
host: config.REDIS_HOSTNAME,
+1 -1
View File
@@ -189,7 +189,7 @@ async function webView(
if (f.extension() == "md") {
try {
const content = await streamToString(await f.anonymizedContent());
const body = sanitizeHtml(marked.marked(content, { headerIds: false, mangle: false }), sanitizeOptions);
const body = sanitizeHtml(marked.marked.parse(content, { async: false }), sanitizeOptions);
const html = `<!DOCTYPE html><html><head><title>Content</title></head><link rel="stylesheet" href="/css/all.min.css" /><body><div class="container p-3 file-content markdown-body">${body}</div></body></html>`;
res.contentType("text/html").send(html);
} catch {