mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-19 06:07:21 +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
@@ -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();
|
||||
|
||||
@@ -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
@@ -74,7 +74,7 @@ function getRedis(): RedisClientType | null {
|
||||
const c = redisClient;
|
||||
redisClient = null;
|
||||
if (c?.isOpen) {
|
||||
c.disconnect().catch(() => {});
|
||||
c.destroy();
|
||||
}
|
||||
});
|
||||
redisClient.connect().catch(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user