diff --git a/app/src/features/share/shareContent.test.ts b/app/src/features/share/shareContent.test.ts index db6497fd0b..e202d0cdb2 100644 --- a/app/src/features/share/shareContent.test.ts +++ b/app/src/features/share/shareContent.test.ts @@ -48,6 +48,54 @@ describe('redactSensitive', () => { expect(redactSensitive('mail jane.doe@example.com now')).toContain('[email]'); }); + // The last-resort rule requires an upper-case character so it does not fire on + // prose. Everything below is lower-case + digits, so it reached the card intact + // until these patterns were added. Each value is a shape-accurate dummy, not a + // real credential. + // + // Every vendor-prefixed value is assembled from parts on purpose: written as a + // single literal it carries the vendor's exact token shape, which is what a + // secret scanner keys on. GitHub push protection already rejects the Slack form + // outright, and that would block this file for anyone pushing it. + const assembled = (separator: string, ...parts: string[]) => parts.join(separator); + + test.each([ + ['slack bot token', assembled('-', 'xoxb', '123456789012', '987654321098', 'abcdefghijklmnop')], + [ + 'slack user token', + assembled('-', 'xoxp', '987654321098', '123456789012', 'zyxwvutsrqponmlk'), + ], + ['gitlab personal access token', assembled('-', 'glpat', 'abcdefghij1234567890')], + ['huggingface token', assembled('_', 'hf', 'abcdefghijklmnopqrstuvwxyz1234')], + ])('scrubs an all-lower-case %s', (_label, secret) => { + const out = redactSensitive(`saved ${secret} for you`); + expect(out).toContain('[redacted]'); + expect(out).not.toContain(secret); + }); + + test.each([ + ['slack', 'https://hooks.slack.com/services/T00000000/B00000000/abcdefghijklmnopqrst'], + [ + 'discord', + 'https://discord.com/api/webhooks/123456789012345678/abcdefghijklmnopqrstuvwxyz012345', + ], + ])('scrubs a %s webhook URL, which carries its secret in the path', (_label, url) => { + expect(redactSensitive(`posting to ${url} now`)).not.toContain(url); + }); + + test('survives stripMarkdown running first, as buildFallbackHeadline runs it', () => { + // stripMarkdown removes `_`, so a pattern that insisted on it would never match. + const hfToken = assembled('_', 'hf', 'abcdefghijklmnopqrstuvwxyz1234'); + const head = buildFallbackHeadline(`Done. Saved ${hfToken} for you.`); + expect(head).not.toContain('abcdefghijklmnopqrstuvwxyz'); + expect(head).toContain('[redacted]'); + }); + + test('does not fire on ordinary lower-case prose', () => { + const clean = 'renamed the folder to archive and moved twelve files into it'; + expect(redactSensitive(clean)).toBe(clean); + }); + test('is idempotent and leaves clean prose untouched', () => { const clean = 'Summarised three months of emails in twelve seconds'; expect(redactSensitive(clean)).toBe(clean); diff --git a/app/src/features/share/shareContent.ts b/app/src/features/share/shareContent.ts index f7efa84475..63c6f144e0 100644 --- a/app/src/features/share/shareContent.ts +++ b/app/src/features/share/shareContent.ts @@ -36,6 +36,19 @@ const REDACTIONS: ReadonlyArray<{ re: RegExp; with: string }> = [ { re: /\bBearer\s+[A-Za-z0-9._-]{12,}\b/gi, with: '[redacted]' }, // AWS access key ids. { re: /\bAKIA[0-9A-Z]{16}\b/g, with: '[redacted]' }, + // Vendor-prefixed credentials whose body is lower-case and digits only. The + // last-resort rule below cannot reach these: it requires an upper-case letter so + // it does not fire on prose, and an all-lower-case token slips straight past it. + // The `_` is optional because `stripMarkdown` strips emphasis characters before + // this runs, so `hf_abc…` arrives here as `hfabc…`. + { re: /\b(?:xox[abeprs]|xapp)-[A-Za-z0-9-]{10,}/g, with: '[redacted]' }, + { re: /\bglpat-[A-Za-z0-9_-]{16,}/g, with: '[redacted]' }, + { re: /\bhf_?[a-z0-9]{20,}\b/g, with: '[redacted]' }, + // Webhook URLs carry the secret in the path, so the whole URL has to go. + { + re: /\bhttps?:\/\/(?:hooks\.slack\.com|discord(?:app)?\.com\/api\/webhooks)\/[^\s"'`)]+/gi, + with: '[redacted]', + }, // Long opaque hex runs (>= 32 chars) that look like secrets, not prose. { re: /\b[A-Fa-f0-9]{32,}\b/g, with: '[redacted]' }, // Long opaque base64/base64url runs (>= 24 chars) that mix upper/lower/digit,