mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-23 16:41:22 +08:00
fix(security): discord bot SSRF/log-injection/DoS hardening + bump markdown-it/js-yaml
- ecc-bot.mjs: validate interaction id (snowflake) and token before building the callback fetch URL (clears CodeQL js/request-forgery #239/#240/#241); clamp the remote heartbeat_interval to [1s,10m] (js/resource-exhaustion #242); strip CR/LF from log args (js/log-injection #246). - Bump transitive dev deps via overrides/resolutions to patch quadratic-complexity DoS: markdown-it >=14.2.0 (Dependabot #45/#46), js-yaml >=4.2.0 (#42/#43). Both lockfiles regenerated; npm reports 0 vulnerabilities.
This commit is contained in:
@@ -26,7 +26,35 @@ const REPO_URL = 'https://github.com/affaan-m/ECC';
|
||||
const INVITE = process.env.DISCORD_INVITE || '';
|
||||
const API = 'https://discord.com/api/v10';
|
||||
|
||||
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
||||
// Strip CR/LF from string args so Discord-payload-controlled values (command
|
||||
// names, usernames) cannot forge or inject extra log lines (log injection).
|
||||
const log = (...a) =>
|
||||
console.log(new Date().toISOString(), ...a.map(x => (typeof x === 'string' ? x.replace(/[\r\n]+/g, ' ') : x)));
|
||||
|
||||
// Interaction ids are Discord snowflakes (numeric) and tokens are a bounded
|
||||
// URL-safe set. Validate before building the callback URL so a malformed or
|
||||
// hostile gateway payload cannot inject path segments / alter the request
|
||||
// target (SSRF). The host is always the fixed API constant.
|
||||
const SNOWFLAKE_RE = /^[0-9]{1,20}$/;
|
||||
const INTERACTION_TOKEN_RE = /^[A-Za-z0-9._-]{1,255}$/;
|
||||
|
||||
function interactionCallbackUrl(interaction) {
|
||||
const id = String(interaction?.id ?? '');
|
||||
const token = String(interaction?.token ?? '');
|
||||
if (!SNOWFLAKE_RE.test(id) || !INTERACTION_TOKEN_RE.test(token)) {
|
||||
throw new Error('invalid interaction id/token');
|
||||
}
|
||||
return `${API}/interactions/${id}/${token}/callback`;
|
||||
}
|
||||
|
||||
// Clamp a remote-supplied timer interval to a sane range so a hostile/bogus
|
||||
// heartbeat_interval cannot spin a tight loop or hang the bot (resource
|
||||
// exhaustion). Discord's real value is ~41250ms.
|
||||
function clampHeartbeatInterval(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return 41250;
|
||||
return Math.max(1000, Math.min(n, 600000));
|
||||
}
|
||||
|
||||
// ---------- skill + docs lookup (local clone as the data source) ----------
|
||||
|
||||
@@ -148,7 +176,13 @@ const handlers = {
|
||||
async function respond(interaction) {
|
||||
const name = interaction.data?.name;
|
||||
const handler = handlers[name];
|
||||
const url = `${API}/interactions/${interaction.id}/${interaction.token}/callback`;
|
||||
let url;
|
||||
try {
|
||||
url = interactionCallbackUrl(interaction);
|
||||
} catch (err) {
|
||||
log('rejected interaction', err.message);
|
||||
return;
|
||||
}
|
||||
if (!handler) {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -192,7 +226,7 @@ async function main() {
|
||||
if (msg.s) seq = msg.s;
|
||||
switch (msg.op) {
|
||||
case 10: { // HELLO
|
||||
const interval = msg.d.heartbeat_interval;
|
||||
const interval = clampHeartbeatInterval(msg.d.heartbeat_interval);
|
||||
setTimeout(() => {
|
||||
send({ op: 1, d: seq });
|
||||
setInterval(() => {
|
||||
|
||||
Reference in New Issue
Block a user