diff --git a/src/db/migrations/0037-remove-leaked-webhooks.js b/src/db/migrations/0037-remove-leaked-webhooks.js new file mode 100644 index 0000000..79fad16 --- /dev/null +++ b/src/db/migrations/0037-remove-leaked-webhooks.js @@ -0,0 +1,41 @@ +const {discord, db, from, select, sync} = require("../../passthrough") +/** @type {import("../../discord/utils")} */ +const dUtils = sync.require("../../discord/utils") + +const ones = "₀₁₂₃₄₅₆₇₈₉" +const tens = "0123456789" + +/* c8 ignore start */ + +module.exports = async function(db) { + // added tolerance to https://discordstatus.com/incidents/4hpm4454hxtx + const OUTAGE_START = 1778263200000 + const OUTAGE_END = 1778284800000 + + const startSnowflake = dUtils.timestampToSnowflakeInexact(OUTAGE_START) + const endSnowflake = dUtils.timestampToSnowflakeInexact(OUTAGE_END) + + const affectedChannels = from("message_room").join("historical_channel_room", "historical_room_index") + .pluck("reference_channel_id").selectUnsafe("DISTINCT reference_channel_id") + .and("WHERE message_id >= ? AND message_id <= ? AND length(message_id) = ?").all(startSnowflake, endSnowflake, startSnowflake.length) + const affectedWebhooks = select("webhook", ["channel_id", "webhook_id", "webhook_token"], {channel_id: affectedChannels}).all() + + if (affectedWebhooks.length) { + process.stdout.write(` revoking ${affectedWebhooks.length} possibly compromised webhooks... `) + for (let counter = 1; counter <= affectedWebhooks.length; counter++) { + const webhook = affectedWebhooks[counter-1] + + await discord.snow.webhook.deleteWebhookToken(webhook.webhook_id, webhook.webhook_token, "Webhook token possibly compromised during 8th May 2026 outage").catch(e => { + if (e.message === `{"message": "Unknown Webhook", "code": 10015}`) { + // OK + } else { + throw e + } + }) + db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(webhook.channel_id) + + process.stdout.write(String(counter).at(-1) === "0" ? tens[(counter/10)%10] : ones[counter%10]) + } + process.stdout.write("\n") + } +} diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index c11b696..0d1c529 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -94,6 +94,11 @@ function printError(type, source, e, payload) { console.dir(payload, {depth: null}) } +/** @param {string} stack */ +function cleanErrorStack(stack) { + return stack.replace(/(\/webhooks\/[0-9]+\/)[a-zA-Z0-9_-]+/g, "$1(redacted)") +} + /** * @param {string} roomID * @param {"Discord" | "Matrix"} source @@ -134,7 +139,7 @@ async function sendError(roomID, source, type, e, payload) { builder.addLine(errorIntroLine) // Where - const stack = stringifyErrorStack(e) + const stack = cleanErrorStack(stringifyErrorStack(e)) builder.addLine(`Error trace:\n${stack}`, tag`
Error trace
${stack}
`) // How @@ -502,5 +507,6 @@ async event => { })) module.exports.stringifyErrorStack = stringifyErrorStack +module.exports.cleanErrorStack = cleanErrorStack module.exports.sendError = sendError module.exports.printError = printError diff --git a/src/m2d/event-dispatcher.test.js b/src/m2d/event-dispatcher.test.js index de754da..2de6381 100644 --- a/src/m2d/event-dispatcher.test.js +++ b/src/m2d/event-dispatcher.test.js @@ -1,7 +1,7 @@ // @ts-check const {test} = require("supertape") -const {stringifyErrorStack} = require("./event-dispatcher") +const {stringifyErrorStack, cleanErrorStack} = require("./event-dispatcher") test("stringify error stack: works", t => { function a() { @@ -21,3 +21,30 @@ test("stringify error stack: works", t => { t.match(str, /^ \[prop\]: 2.1$/m) } }) + +test("clean error stack: removes webhook token", t => { + t.notMatch( + cleanErrorStack(` + DiscordAPIError: Service resource is being rate limited. + at fn (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:591:13) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + at exports.RequestHandler.request (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/RequestHandler.ts:546:17) + at WebhookMethods.executeWebhook (/var/home/cadence/out-of-your-element/node_modules/snowtransfer/src/methods/Webhook.ts:249:35) + at /var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:65:31 + at withWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:47:9) + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + at async Object.sendMessageWithWebhook (/var/home/cadence/out-of-your-element/src/m2d/actions/channel-webhook.js:64:17) + at async Object.sendEvent (/var/home/cadence/out-of-your-element/src/m2d/actions/send-event.js:132:27) + at async /var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:208:27 + at async AppService. (/var/home/cadence/out-of-your-element/src/m2d/event-dispatcher.js:162:11) { + [method]: "POST" + [path]: "/webhooks/1160903754728611841/pfRqHl9vVZImdqwWWSZxxH8T-JJMnauxroMnHsvC6ARA-3B9_STH_bnHB9pd7QQaUVCG" + [code]: 40062 + [httpStatus]: 429 + [request]: {"endpoint":"/webhooks/1160903754728611841/pfRqHl9vVZImdqwWWSZxxH8T-JJMnauxroMnHsvC6ARA-3B9_STH_bnHB9pd7QQaUVCG","method":"POST","dataType":"json","data":{"content":"https://discordstatus.com/#day\nOnly what discord tell us right now","allowed_mentions":{"parse":["roles"],"users":[]},"username":"lewri","avatar_url":"https://bridge.cadence.moe/download/matrix/matrix.org/URWwrtSUONGOYhfMsdUzcrir"}} + [response]: {} + [name]: "DiscordAPIError"` + ), + /pfRqHl9v/ + ) +})