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:
Affaan Mustafa 2026-06-18 19:50:15 -04:00
parent a03d63cba0
commit 5e4f5533d7
4 changed files with 161 additions and 80 deletions

58
package-lock.json generated
View File

@ -517,7 +517,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -989,7 +988,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -1612,10 +1610,20 @@
}
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@ -1720,10 +1728,21 @@
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
"integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
@ -1778,14 +1797,25 @@
}
},
"node_modules/markdown-it": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
"integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"linkify-it": "^5.0.1",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
@ -2664,7 +2694,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -3002,7 +3031,8 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.24.6",

View File

@ -374,5 +374,13 @@
"engines": {
"node": ">=18"
},
"overrides": {
"markdown-it": ">=14.2.0",
"js-yaml": ">=4.2.0"
},
"resolutions": {
"markdown-it": ">=14.2.0",
"js-yaml": ">=4.2.0"
},
"packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c"
}

View File

@ -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(() => {

135
yarn.lock
View File

@ -221,16 +221,16 @@ __metadata:
linkType: hard
"@opencode-ai/plugin@npm:^1.16.2":
version: 1.16.2
resolution: "@opencode-ai/plugin@npm:1.16.2"
version: 1.17.3
resolution: "@opencode-ai/plugin@npm:1.17.3"
dependencies:
"@opencode-ai/sdk": "npm:1.16.2"
"@opencode-ai/sdk": "npm:1.17.3"
effect: "npm:4.0.0-beta.74"
zod: "npm:4.1.8"
peerDependencies:
"@opentui/core": ">=0.3.2"
"@opentui/keymap": ">=0.3.2"
"@opentui/solid": ">=0.3.2"
"@opentui/core": ">=0.3.4"
"@opentui/keymap": ">=0.3.4"
"@opentui/solid": ">=0.3.4"
peerDependenciesMeta:
"@opentui/core":
optional: true
@ -238,16 +238,16 @@ __metadata:
optional: true
"@opentui/solid":
optional: true
checksum: 10c0/a10d6259557c2b587afd93dc690bf5b3b0132272c0b0bca1f77c5a7ea9e3440ebbf1fbdf0a24e4a20ae05f23961c546a2feab53c8f50b10a9d237090c41d2146
checksum: 10c0/c78d3915ca1e479d638230d4f4a2f439163691c45e88284a6862f53ca349916845bf97ea08b40d59acb00b9af7522d2b45f399b420cfb17cfc2a3db3b02653cc
languageName: node
linkType: hard
"@opencode-ai/sdk@npm:1.16.2":
version: 1.16.2
resolution: "@opencode-ai/sdk@npm:1.16.2"
"@opencode-ai/sdk@npm:1.17.3":
version: 1.17.3
resolution: "@opencode-ai/sdk@npm:1.17.3"
dependencies:
cross-spawn: "npm:7.0.6"
checksum: 10c0/0f8de5a64bfb4411ecddd0bb2c3049df07a5661a05e7c965e2404692f80394652b00a4d46985e97aabf8b26ac85e4313d8dbf7bce2280a297eb4fa88bf700a6a
checksum: 10c0/5de73b708545623640a03bafd0618961777201c82d542570eca65fe43a485e095ce74d687042cb290fa9a8c3c480e2ed2dba7b02a951ea2f0f3c68cdc7208560
languageName: node
linkType: hard
@ -318,10 +318,10 @@ __metadata:
languageName: node
linkType: hard
"abbrev@npm:^4.0.0":
version: 4.0.0
resolution: "abbrev@npm:4.0.0"
checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5
"abbrev@npm:^5.0.0":
version: 5.0.0
resolution: "abbrev@npm:5.0.0"
checksum: 10c0/8e88f5c798ea4562d28c5a3e9ad69e3879890bc5d695d8f2dffb8609be4c890aacc8f80ef4553fdd2c6a62d70c2ce8bc57b38074e383beb7487bdafa9ed42ea5
languageName: node
linkType: hard
@ -344,26 +344,26 @@ __metadata:
linkType: hard
"ajv@npm:^6.12.4":
version: 6.15.0
resolution: "ajv@npm:6.15.0"
version: 6.14.0
resolution: "ajv@npm:6.14.0"
dependencies:
fast-deep-equal: "npm:^3.1.1"
fast-json-stable-stringify: "npm:^2.0.0"
json-schema-traverse: "npm:^0.4.1"
uri-js: "npm:^4.2.2"
checksum: 10c0/67966499dd272ecde1c2e467084411132891523d057487587879d39ac04207f4351b7b2324c83198013967fbfa632c1612adc960114a30770fbe07a0773b32c2
checksum: 10c0/a2bc39b0555dc9802c899f86990eb8eed6e366cddbf65be43d5aa7e4f3c4e1a199d5460fd7ca4fb3d864000dbbc049253b72faa83b3b30e641ca52cb29a68c22
languageName: node
linkType: hard
"ajv@npm:^8.18.0":
version: 8.20.0
resolution: "ajv@npm:8.20.0"
version: 8.18.0
resolution: "ajv@npm:8.18.0"
dependencies:
fast-deep-equal: "npm:^3.1.3"
fast-uri: "npm:^3.0.1"
json-schema-traverse: "npm:^1.0.0"
require-from-string: "npm:^2.0.2"
checksum: 10c0/5df9a1c8f83863cde1bd3a9ddb426f599718f88e3dc9153616c79fb28e0be455335830d7f21d745576519f057b371352daa31047b6a33d7036fe08777d60cf2a
checksum: 10c0/e7517c426173513a07391be951879932bdf3348feaebd2199f5b901c20f99d60db8cd1591502d4d551dc82f594e82a05c4fe1c70139b15b8937f7afeaed9532f
languageName: node
linkType: hard
@ -967,9 +967,9 @@ __metadata:
linkType: hard
"globals@npm:^17.4.0":
version: 17.6.0
resolution: "globals@npm:17.6.0"
checksum: 10c0/cf94fb4329cc5c68cf81018fd68324f413181ee169f0235b0b33b82bc93fe7825a21beea951f83a80e8e4bbdad9c0c80515a145b5fd4b5cb52f2a80db899a93f
version: 17.4.0
resolution: "globals@npm:17.4.0"
checksum: 10c0/2be9e8c2b9035836f13d420b22f0247a328db82967d3bebfc01126d888ed609305f06c05895914e969653af5c6ba35fd7a0920f3e6c869afa60666c810630feb
languageName: node
linkType: hard
@ -1135,14 +1135,14 @@ __metadata:
languageName: node
linkType: hard
"js-yaml@npm:^4.1.1, js-yaml@npm:~4.1.1":
version: 4.1.1
resolution: "js-yaml@npm:4.1.1"
"js-yaml@npm:>=4.2.0":
version: 4.2.0
resolution: "js-yaml@npm:4.2.0"
dependencies:
argparse: "npm:^2.0.1"
bin:
js-yaml: bin/js-yaml.js
checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7
checksum: 10c0/1916456c118746603b067d74bbcbb0445d9a1d5e474ad4ae775e7b20525bed902e01d9d97dd0c81fcd8d4f596162309d0eb057f4aa38f3e9647f14075e9dea45
languageName: node
linkType: hard
@ -1225,12 +1225,12 @@ __metadata:
languageName: node
linkType: hard
"linkify-it@npm:^5.0.0":
version: 5.0.0
resolution: "linkify-it@npm:5.0.0"
"linkify-it@npm:^5.0.1":
version: 5.0.1
resolution: "linkify-it@npm:5.0.1"
dependencies:
uc.micro: "npm:^2.0.0"
checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
checksum: 10c0/d06d04f1ed03be131740fc900a5e74ea1f49886b052213599e306d469d5ffe2303db76dd8f771de9f28e2b0b38852de22ec46ae597d245f8b66439b0ceb19b10
languageName: node
linkType: hard
@ -1266,19 +1266,19 @@ __metadata:
languageName: node
linkType: hard
"markdown-it@npm:~14.1.1":
version: 14.1.1
resolution: "markdown-it@npm:14.1.1"
"markdown-it@npm:>=14.2.0":
version: 14.2.0
resolution: "markdown-it@npm:14.2.0"
dependencies:
argparse: "npm:^2.0.1"
entities: "npm:^4.4.0"
linkify-it: "npm:^5.0.0"
linkify-it: "npm:^5.0.1"
mdurl: "npm:^2.0.0"
punycode.js: "npm:^2.3.1"
uc.micro: "npm:^2.1.0"
bin:
markdown-it: bin/markdown-it.mjs
checksum: 10c0/c67f2a4c8069a307c78d8c15104bbcb15a2c6b17f4c904364ca218ec2eccf76a397eba1ea05f5ac5de72c4b67fcf115d422d22df0bfb86a09b663f55b9478d4f
checksum: 10c0/1d3a50061d2fe4efbcf317aac853dbee6892ed6f5a217570eead723f2ef2dd1c9baaeef5a687cd283480c45c2d20724a73e84a9ed72843cf7b3b719067af40ef
languageName: node
linkType: hard
@ -1742,33 +1742,33 @@ __metadata:
linkType: hard
"node-gyp@npm:latest":
version: 12.3.0
resolution: "node-gyp@npm:12.3.0"
version: 13.0.0
resolution: "node-gyp@npm:13.0.0"
dependencies:
env-paths: "npm:^2.2.0"
exponential-backoff: "npm:^3.1.1"
graceful-fs: "npm:^4.2.6"
nopt: "npm:^9.0.0"
proc-log: "npm:^6.0.0"
nopt: "npm:^10.0.0"
proc-log: "npm:^7.0.0"
semver: "npm:^7.3.5"
tar: "npm:^7.5.4"
tinyglobby: "npm:^0.2.12"
undici: "npm:^6.25.0"
which: "npm:^6.0.0"
which: "npm:^7.0.0"
bin:
node-gyp: bin/node-gyp.js
checksum: 10c0/9d9032b405cbe42f72a105259d9eb679376470c102df4a2dbaa51e07d59bf741dcffb85897087ea9d8318b9cabb824a8978af51508ae142f0239ae1e6a3c2329
checksum: 10c0/e7525c427db2d16aa368b8947187de83083d2a8dda23e3e096a71c22ae637ac5bb8ed7cf6c871f1b9118cd2729dbfee4ff3a4245e2b79226900227b15831b492
languageName: node
linkType: hard
"nopt@npm:^9.0.0":
version: 9.0.0
resolution: "nopt@npm:9.0.0"
"nopt@npm:^10.0.0":
version: 10.0.1
resolution: "nopt@npm:10.0.1"
dependencies:
abbrev: "npm:^4.0.0"
abbrev: "npm:^5.0.0"
bin:
nopt: bin/nopt.js
checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd
checksum: 10c0/980d89257f9587f3e1f77877ddbf905d6aa3b738ec33e49a4fa1a059a0dd82eb28063982b150654a7ae9de386f2ead60e56172db7d37cf56de545f7392a2a26a
languageName: node
linkType: hard
@ -1866,10 +1866,10 @@ __metadata:
languageName: node
linkType: hard
"proc-log@npm:^6.0.0":
version: 6.1.0
resolution: "proc-log@npm:6.1.0"
checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82
"proc-log@npm:^7.0.0":
version: 7.0.0
resolution: "proc-log@npm:7.0.0"
checksum: 10c0/b89c2d862604f35fec795477b0c7e376feab3ba0d4f4d291c4e959567442697cf451ac557d0623c1cc38af45a78128b983410f397a10c5d3a67f76c33de4754b
languageName: node
linkType: hard
@ -1929,7 +1929,16 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.3":
"semver@npm:^7.3.5":
version: 7.8.4
resolution: "semver@npm:7.8.4"
bin:
semver: bin/semver.js
checksum: 10c0/81b7c296fd7927b80f67fa516b75fa1017caac8167795320de28e76ccbc6f7f01763c30ecd10d6a0d8fd089708ab0548a5aebb94b0870e99c2a2b4600a46389b
languageName: node
linkType: hard
"semver@npm:^7.5.3":
version: 7.7.4
resolution: "semver@npm:7.7.4"
bin:
@ -2055,12 +2064,12 @@ __metadata:
linkType: hard
"tinyglobby@npm:^0.2.12":
version: 0.2.16
resolution: "tinyglobby@npm:0.2.16"
version: 0.2.17
resolution: "tinyglobby@npm:0.2.17"
dependencies:
fdir: "npm:^6.5.0"
picomatch: "npm:^4.0.4"
checksum: 10c0/f2e09fd93dd95c41e522113b686ff6f7c13020962f8698a864a257f3d7737599afc47722b7ab726e12f8a813f779906187911ff8ee6701ede65072671a7e934b
checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c
languageName: node
linkType: hard
@ -2125,9 +2134,9 @@ __metadata:
linkType: hard
"undici@npm:^6.25.0":
version: 6.25.0
resolution: "undici@npm:6.25.0"
checksum: 10c0/2597cc6689bdb02c210c557b1f85febbfda65becae6e6fc1061508e2f33734d25207f81cd8af56ada9956329eb3a7bd7431e87dcfeceba20ee87059b57dcf985
version: 6.27.0
resolution: "undici@npm:6.27.0"
checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f
languageName: node
linkType: hard
@ -2171,14 +2180,14 @@ __metadata:
languageName: node
linkType: hard
"which@npm:^6.0.0":
version: 6.0.1
resolution: "which@npm:6.0.1"
"which@npm:^7.0.0":
version: 7.0.0
resolution: "which@npm:7.0.0"
dependencies:
isexe: "npm:^4.0.0"
bin:
node-which: bin/which.js
checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5
checksum: 10c0/ca0b54f198f78bbc4b7c02e34bda8d335cb352e0adb4cbca1c37b1a957af3a879a82c4c27ca6525bc942f548d8b64f816ef6528360af9f3de55ffb9b979b620d
languageName: node
linkType: hard