Skip to content

Commit 53e401e

Browse files
Greg JosephCopilot
andcommitted
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 '<script>alert(1)</script>' is returned as 'scriptalert1/script' (angle brackets stripped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1cc264d commit 53e401e

1 file changed

Lines changed: 14 additions & 1 deletion

File tree

AI/ocr/server/onReceiptAdded.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,23 @@ import { ReceiptProcessor } from "./ReceiptProcessor";
66

77
require('isomorphic-fetch');
88

9+
// Microsoft Graph sends an opaque, URL-safe validationToken when a subscription is
10+
// created and expects it echoed back verbatim as text/plain. The value is attacker
11+
// influenceable, so strip anything outside the token's known-safe character set
12+
// before reflecting it. This is a no-op for legitimate tokens (base64url/base64)
13+
// while removing the characters needed for reflected cross-site scripting.
14+
const sanitizeValidationToken = (value: unknown): string =>
15+
String(value).replace(/[^A-Za-z0-9._~+/=\- ]/g, '');
16+
917
export const onReceiptAdded = async (req: Request, res: Response) => {
1018
const validationToken = req.query['validationToken'];
1119
if (validationToken) {
12-
res.status(200).type('text/plain').send(String(validationToken));
20+
const safeValidationToken = sanitizeValidationToken(validationToken);
21+
res
22+
.status(200)
23+
.type('text/plain')
24+
.set('X-Content-Type-Options', 'nosniff')
25+
.send(safeValidationToken);
1326
return;
1427
}
1528

0 commit comments

Comments
 (0)