From 53e401ea7bac0a805d1e57958c092da9cae208ca Mon Sep 17 00:00:00 2001 From: Greg Joseph Date: Fri, 3 Jul 2026 16:18:54 -0700 Subject: [PATCH] fix(ocr): sanitize Graph validationToken echo to prevent reflected XSS CodeQL js/reflected-xss (high) flagged AI/ocr/server/onReceiptAdded.ts: the Microsoft Graph subscription validationToken from req.query was echoed straight into the HTTP response body. Although the response is sent as text/plain, a user-controlled value reflected verbatim is a reflected XSS sink (browsers can MIME-sniff, and the value is attacker influenceable). Graph requires the opaque, URL-safe validationToken to be echoed back to complete the subscription handshake, so strip any character outside the token's known-safe set (base64url/base64) before reflecting it. This is a no-op for legitimate tokens while removing the characters needed for XSS. Also set X-Content-Type-Options: nosniff as defense in depth. Verified on Windows (Node 24): backend builds; validate-sample.ps1 backend smoke passes; a legitimate token echoes verbatim (handshake intact) while '' is returned as 'scriptalert1/script' (angle brackets stripped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AI/ocr/server/onReceiptAdded.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/AI/ocr/server/onReceiptAdded.ts b/AI/ocr/server/onReceiptAdded.ts index e5f0fb9..ffbad34 100644 --- a/AI/ocr/server/onReceiptAdded.ts +++ b/AI/ocr/server/onReceiptAdded.ts @@ -6,10 +6,23 @@ import { ReceiptProcessor } from "./ReceiptProcessor"; require('isomorphic-fetch'); + // Microsoft Graph sends an opaque, URL-safe validationToken when a subscription is + // created and expects it echoed back verbatim as text/plain. The value is attacker + // influenceable, so strip anything outside the token's known-safe character set + // before reflecting it. This is a no-op for legitimate tokens (base64url/base64) + // while removing the characters needed for reflected cross-site scripting. + const sanitizeValidationToken = (value: unknown): string => + String(value).replace(/[^A-Za-z0-9._~+/=\- ]/g, ''); + export const onReceiptAdded = async (req: Request, res: Response) => { const validationToken = req.query['validationToken']; if (validationToken) { - res.status(200).type('text/plain').send(String(validationToken)); + const safeValidationToken = sanitizeValidationToken(validationToken); + res + .status(200) + .type('text/plain') + .set('X-Content-Type-Options', 'nosniff') + .send(safeValidationToken); return; }