From 9ea2916c7ad0dc633f864c92829c58d0b225045c Mon Sep 17 00:00:00 2001 From: batmanisko Date: Tue, 14 Jul 2026 11:30:03 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20redigov=C3=A1n=C3=AD=20citliv=C3=BDch=20?= =?UTF-8?q?dat=20p=C5=99ed=20odesl=C3=A1n=C3=ADm=20do=20Sentry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Console eventy nesou celé argumenty console.error — u axios chyb tedy i config s hlavičkami a request body (Bolt token, Gotify/NTFY klíče). beforeSend nově rekurzivně rediguje citlivé klíče a tokeny v řetězcích napříč extra/breadcrumbs/contexts/request/exception. --- server/src/instrument.ts | 32 +++++++++++++++++++++++++++++ server/src/tests/instrument.test.ts | 28 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 server/src/tests/instrument.test.ts diff --git a/server/src/instrument.ts b/server/src/instrument.ts index 0bf22ae..dba7cd1 100644 --- a/server/src/instrument.ts +++ b/server/src/instrument.ts @@ -6,6 +6,30 @@ import path from 'path'; // proto si tento soubor načítá .env sám — index.ts na to dojde až po importech. dotenv.config({ path: path.resolve(__dirname, `../.env.${process.env.NODE_ENV ?? 'production'}`) }); +/** Klíče, jejichž hodnoty se před odesláním do Sentry redigují (hlavičky, tokeny, API klíče…). */ +const SENSITIVE_KEY = /authorization|cookie|token|secret|passw|api_?key|(^|[-_])key$/i; +/** Tokeny/klíče schované uvnitř řetězců (query parametry, serializovaná JSON body). */ +const SENSITIVE_STRING = /((?:token|key|secret|password)"?\s*[=:]\s*"?)[^&\s"',}]+/gi; + +/** + * Rekurzivně rediguje citlivé hodnoty v datech eventu. Console eventy nesou celé + * argumenty console.error — u axios chyb tedy i config s hlavičkami a request body + * (Bolt token, Gotify/NTFY klíče), které nesmí odejít do Sentry. + */ +export function scrubEventData(value: unknown, depth = 0): unknown { + if (depth > 8) return '[max depth]'; + if (typeof value === 'string') return value.replace(SENSITIVE_STRING, '$1[redacted]'); + if (Array.isArray(value)) return value.map(v => scrubEventData(v, depth + 1)); + if (value && typeof value === 'object') { + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + out[k] = SENSITIVE_KEY.test(k) ? '[redacted]' : scrubEventData(v, depth + 1); + } + return out; + } + return value; +} + // Bez SENTRY_DSN (dev, Jest, E2E) zůstává Sentry úplně vypnuté. if (process.env.SENTRY_DSN) { Sentry.init({ @@ -15,6 +39,14 @@ if (process.env.SENTRY_DSN) { // console.error/warn se hlásí jako eventy, console.log (např. přechody stavů // Bolt trackingu) se k chybám přikládá jako breadcrumbs (výchozí integrace). integrations: [Sentry.captureConsoleIntegration({ levels: ['error', 'warn'] })], + beforeSend(event) { + if (event.extra) event.extra = scrubEventData(event.extra) as typeof event.extra; + if (event.breadcrumbs) event.breadcrumbs = scrubEventData(event.breadcrumbs) as typeof event.breadcrumbs; + if (event.contexts) event.contexts = scrubEventData(event.contexts) as typeof event.contexts; + if (event.request) event.request = scrubEventData(event.request) as typeof event.request; + if (event.exception) event.exception = scrubEventData(event.exception) as typeof event.exception; + return event; + }, }); console.log('Sentry: inicializováno'); } diff --git a/server/src/tests/instrument.test.ts b/server/src/tests/instrument.test.ts new file mode 100644 index 0000000..ebfcd81 --- /dev/null +++ b/server/src/tests/instrument.test.ts @@ -0,0 +1,28 @@ +import { scrubEventData } from '../instrument'; + +describe('scrubEventData', () => { + test('rediguje citlivé klíče (hlavičky, tokeny, API klíče)', () => { + const scrubbed = scrubEventData({ + headers: { Authorization: 'Bearer abc123', 'X-Gotify-Key': 'gk1', accept: 'application/json' }, + data: { token: 'deadbeef'.repeat(8) }, + apiKey: 'k', + }) as any; + expect(scrubbed.headers.Authorization).toBe('[redacted]'); + expect(scrubbed.headers['X-Gotify-Key']).toBe('[redacted]'); + expect(scrubbed.headers.accept).toBe('application/json'); + expect(scrubbed.data.token).toBe('[redacted]'); + expect(scrubbed.apiKey).toBe('[redacted]'); + }); + + test('rediguje tokeny uvnitř řetězců (query parametry, JSON body)', () => { + expect(scrubEventData('https://ntfy.example.com/topic?token=tajny&x=1')).toBe( + 'https://ntfy.example.com/topic?token=[redacted]&x=1' + ); + expect(scrubEventData('{"token":"deadbeef","other":1}')).toBe('{"token":"[redacted]","other":1}'); + }); + + test('nechává běžná data beze změny včetně vnořených polí', () => { + const input = { breadcrumbs: [{ message: 'Bolt tracking: stav accepted → preparing', level: 'log' }], count: 2 }; + expect(scrubEventData(input)).toEqual(input); + }); +});