diff --git a/.changeset/ast-entrypoint-source-positions.md b/.changeset/ast-entrypoint-source-positions.md
new file mode 100644
index 0000000000..67e377079e
--- /dev/null
+++ b/.changeset/ast-entrypoint-source-positions.md
@@ -0,0 +1,7 @@
+---
+"react-doctor": patch
+"oxlint-plugin-react-doctor": patch
+"eslint-plugin-react-doctor": patch
+---
+
+Classify source positions before reporting textual entrypoint matches in generated-source prose. Preserve existing findings when parsing or a source position is unsupported.
diff --git a/packages/fuzz/corpus/regressions/webhook-signature-risk--actual-entrypoint.tsx b/packages/fuzz/corpus/regressions/webhook-signature-risk--actual-entrypoint.tsx
new file mode 100644
index 0000000000..6cfcce3fbe
--- /dev/null
+++ b/packages/fuzz/corpus/regressions/webhook-signature-risk--actual-entrypoint.tsx
@@ -0,0 +1,5 @@
+// rule: webhook-signature-risk
+// weakness: entrypoint-source-position
+// source: independent synthetic source position control
+// verdict: fail
+export const webhookHandler = (req: { body: unknown }) => req.body;
diff --git a/packages/fuzz/corpus/regressions/webhook-signature-risk--entrypoint-prose.tsx b/packages/fuzz/corpus/regressions/webhook-signature-risk--entrypoint-prose.tsx
new file mode 100644
index 0000000000..b144dbee37
--- /dev/null
+++ b/packages/fuzz/corpus/regressions/webhook-signature-risk--entrypoint-prose.tsx
@@ -0,0 +1,6 @@
+// rule: webhook-signature-risk
+// weakness: entrypoint-source-position
+// source: independent synthetic source position control
+// verdict: pass
+export const renderSource = () => "const payload = req.body;";
+console.error("Could not write webhookHandler output");
diff --git a/packages/fuzz/src/snippet-pools.ts b/packages/fuzz/src/snippet-pools.ts
index accb163dd2..8f80af0ca2 100644
--- a/packages/fuzz/src/snippet-pools.ts
+++ b/packages/fuzz/src/snippet-pools.ts
@@ -314,6 +314,8 @@ export const LIBRARY_SNIPPET_POOL = [
// Module scope — SSR hazards, guard aliases, contexts, caches, styled.
export const MODULE_SCOPE_SNIPPET_POOL = [
+ 'export const renderSource = () => "const payload = req.body;";\nconsole.error("Could not write webhookHandler output");\n',
+ "export const webhookHandler = (req: { body: unknown }) => req.body;\n",
`import { create as createFuzzZustandStore } from "zustand"; export const useFuzzZustandStore = createFuzzZustandStore(() => ({ items: [] })); export const fuzzZustandSelection = useFuzzZustandStore((state) => ({ items: state.items }));`,
`import { create as createFuzzZustandTarget } from "zustand"; const useFuzzZustandTarget = createFuzzZustandTarget(() => ({ value: 0, other: 0 })); export const FuzzZustandWholeStoreTarget = () => { const { value: fuzzZustandValue } = useFuzzZustandTarget(); return ; };`,
`import { create as createFuzzZustandGetStore } from "zustand"; const useFuzzZustandGetStore = createFuzzZustandGetStore((_set, get) => ({ count: get().count }));`,
diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/get-classified-webhook-entrypoint.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/get-classified-webhook-entrypoint.ts
new file mode 100644
index 0000000000..5d0cba5fa8
--- /dev/null
+++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/get-classified-webhook-entrypoint.ts
@@ -0,0 +1,237 @@
+import { CROSS_FILE_PARSE_MAX_BYTES } from "../../../constants/thresholds.js";
+import type { EsTreeNode } from "../../../utils/es-tree-node.js";
+import type { ScannedFile } from "../../../utils/file-scan.js";
+import { getNodeEndIndex } from "../../../utils/get-node-end-index.js";
+import { getNodeStartIndex } from "../../../utils/get-node-start-index.js";
+import { isNodeOfType } from "../../../utils/is-node-of-type.js";
+import { parseSourceText } from "../../../utils/parse-source-file.js";
+import { walkAst } from "../../../utils/walk-ast.js";
+import { getScannableContent } from "./scan-by-pattern.js";
+import {
+ WEBHOOK_FIRST_ITEM_INDEX,
+ WEBHOOK_REGION_STEP_COUNT,
+ WEBHOOK_MARKER_CHAR_COUNT,
+ WEBHOOK_TEMPLATE_EXPRESSION_MARKER_CHAR_COUNT,
+ WEBHOOK_NON_ASCII_PATTERN,
+ WEBHOOK_LITERAL_WHITESPACE_PATTERN,
+ WEBHOOK_IDENTIFIER_NAME_PATTERN,
+ WEBHOOK_FUNCTION_NAME_PATTERN,
+ WEBHOOK_VARIABLE_NAME_PATTERN,
+ WEBHOOK_SYNC_EXPORT_PATTERN,
+ WEBHOOK_ASYNC_EXPORT_PATTERN,
+ WEBHOOK_CONST_EXPORT_PATTERN,
+} from "./webhook-entrypoint/constants.js";
+
+interface EntrypointRegion {
+ readonly start: number;
+ readonly end: number;
+ readonly isRetained: boolean;
+}
+
+export interface ClassifiedWebhookEntrypoint {
+ readonly index: number | null;
+}
+
+const hasValidSourceSpan = (node: EsTreeNode, source: string): boolean => {
+ const start = getNodeStartIndex(node);
+ const end = getNodeEndIndex(node);
+ return (
+ Number.isInteger(start) &&
+ Number.isInteger(end) &&
+ start >= WEBHOOK_FIRST_ITEM_INDEX &&
+ end >= start &&
+ end <= source.length
+ );
+};
+
+const isDirectExportMatch = (
+ node: EsTreeNode,
+ match: RegExpMatchArray,
+ source: string,
+): boolean => {
+ const matchStart = match.index;
+ if (
+ typeof matchStart !== "number" ||
+ !isNodeOfType(node, "ExportNamedDeclaration") ||
+ !node.declaration ||
+ matchStart !== getNodeStartIndex(node) ||
+ !source.startsWith("export", matchStart)
+ )
+ return false;
+ const declaration = node.declaration;
+ if (isNodeOfType(declaration, "FunctionDeclaration")) {
+ return Boolean(
+ declaration.id &&
+ isNodeOfType(declaration.id, "Identifier") &&
+ WEBHOOK_FUNCTION_NAME_PATTERN.test(declaration.id.name) &&
+ !declaration.generator &&
+ (declaration.async ? WEBHOOK_ASYNC_EXPORT_PATTERN : WEBHOOK_SYNC_EXPORT_PATTERN).test(
+ match[WEBHOOK_FIRST_ITEM_INDEX],
+ ) &&
+ getNodeEndIndex(declaration.id) === matchStart + match[WEBHOOK_FIRST_ITEM_INDEX].length &&
+ source.slice(getNodeStartIndex(declaration.id), getNodeEndIndex(declaration.id)) ===
+ declaration.id.name,
+ );
+ }
+ if (
+ !isNodeOfType(declaration, "VariableDeclaration") ||
+ declaration.kind !== "const" ||
+ !WEBHOOK_CONST_EXPORT_PATTERN.test(match[WEBHOOK_FIRST_ITEM_INDEX])
+ )
+ return false;
+ const firstDeclarator = declaration.declarations[WEBHOOK_FIRST_ITEM_INDEX];
+ return Boolean(
+ firstDeclarator &&
+ isNodeOfType(firstDeclarator, "VariableDeclarator") &&
+ isNodeOfType(firstDeclarator.id, "Identifier") &&
+ WEBHOOK_VARIABLE_NAME_PATTERN.test(firstDeclarator.id.name) &&
+ getNodeEndIndex(firstDeclarator.id) === matchStart + match[WEBHOOK_FIRST_ITEM_INDEX].length &&
+ source.slice(getNodeStartIndex(firstDeclarator.id), getNodeEndIndex(firstDeclarator.id)) ===
+ firstDeclarator.id.name,
+ );
+};
+
+export const getClassifiedWebhookEntrypoint = (
+ file: ScannedFile,
+ entrypointPattern: RegExp,
+): ClassifiedWebhookEntrypoint | null => {
+ if (
+ file.content.length > CROSS_FILE_PARSE_MAX_BYTES ||
+ WEBHOOK_NON_ASCII_PATTERN.test(file.content)
+ )
+ return null;
+ try {
+ const program = parseSourceText({
+ filename: file.absolutePath,
+ sourceText: file.content,
+ shouldAttachParentReferences: false,
+ });
+ if (!program) return null;
+ const content = getScannableContent(file);
+ if (content.length !== file.content.length) return null;
+ const pattern = new RegExp(
+ entrypointPattern.source,
+ entrypointPattern.flags.includes("g")
+ ? entrypointPattern.flags
+ : `${entrypointPattern.flags}g`,
+ );
+ const matches = [...content.matchAll(pattern)];
+ if (matches.length === WEBHOOK_FIRST_ITEM_INDEX) return null;
+ const matchesByStart = new Map();
+ for (const match of matches) {
+ if (typeof match.index !== "number" || !Number.isInteger(match.index)) return null;
+ matchesByStart.set(match.index, match);
+ }
+ const regions: EntrypointRegion[] = [];
+ let hasConsistentSpans = true;
+ walkAst(program, (node) => {
+ if (!hasConsistentSpans) return false;
+ if (!hasValidSourceSpan(node, file.content)) {
+ hasConsistentSpans = false;
+ return false;
+ }
+ const start = getNodeStartIndex(node);
+ const end = getNodeEndIndex(node);
+ if (
+ isNodeOfType(node, "Identifier") &&
+ WEBHOOK_IDENTIFIER_NAME_PATTERN.test(node.name) &&
+ file.content.slice(start, end) === node.name
+ ) {
+ regions.push({ start, end, isRetained: true });
+ } else if (isNodeOfType(node, "ExportNamedDeclaration")) {
+ const match = matchesByStart.get(start);
+ if (match && isDirectExportMatch(node, match, file.content))
+ regions.push({
+ start,
+ end: start + match[WEBHOOK_FIRST_ITEM_INDEX].length,
+ isRetained: true,
+ });
+ } else if (isNodeOfType(node, "Literal")) {
+ const rawSource = file.content.slice(start, end);
+ if ("regex" in node && node.regex) {
+ if (rawSource !== `/${node.regex.pattern}/${node.regex.flags}`) {
+ hasConsistentSpans = false;
+ return false;
+ }
+ regions.push({
+ start: start + WEBHOOK_MARKER_CHAR_COUNT,
+ end: end - node.regex.flags.length - WEBHOOK_MARKER_CHAR_COUNT,
+ isRetained: false,
+ });
+ } else if (typeof node.value === "string") {
+ const marker = rawSource[WEBHOOK_FIRST_ITEM_INDEX];
+ if ((marker !== '"' && marker !== "'") || !rawSource.endsWith(marker)) {
+ hasConsistentSpans = false;
+ return false;
+ }
+ const interior = rawSource.slice(WEBHOOK_MARKER_CHAR_COUNT, -WEBHOOK_MARKER_CHAR_COUNT);
+ if (interior.includes("\\")) return;
+ if (interior !== node.value) {
+ hasConsistentSpans = false;
+ return false;
+ }
+ regions.push({
+ start: start + WEBHOOK_MARKER_CHAR_COUNT,
+ end: end - WEBHOOK_MARKER_CHAR_COUNT,
+ isRetained: !WEBHOOK_LITERAL_WHITESPACE_PATTERN.test(interior),
+ });
+ }
+ } else if (isNodeOfType(node, "TemplateElement")) {
+ const rawSource = file.content.slice(start, end);
+ const marker = rawSource[WEBHOOK_FIRST_ITEM_INDEX];
+ const ending = node.tail ? "`" : "${";
+ const endingLength = node.tail
+ ? WEBHOOK_MARKER_CHAR_COUNT
+ : WEBHOOK_TEMPLATE_EXPRESSION_MARKER_CHAR_COUNT;
+ if (
+ (marker !== "`" && marker !== "}") ||
+ !rawSource.endsWith(ending) ||
+ rawSource.slice(WEBHOOK_MARKER_CHAR_COUNT, -endingLength) !== node.value.raw
+ ) {
+ hasConsistentSpans = false;
+ return false;
+ }
+ regions.push({
+ start: start + WEBHOOK_MARKER_CHAR_COUNT,
+ end: end - endingLength,
+ isRetained: false,
+ });
+ }
+ });
+ if (
+ !hasConsistentSpans ||
+ regions.some(
+ (region) =>
+ region.start < WEBHOOK_FIRST_ITEM_INDEX ||
+ region.end < region.start ||
+ region.end > file.content.length,
+ )
+ )
+ return null;
+ regions.sort((left, right) => left.start - right.start || left.end - right.end);
+ let regionIndex = WEBHOOK_FIRST_ITEM_INDEX;
+ let firstRetainedIndex: number | null = null;
+ for (const match of matches) {
+ const index = match.index;
+ if (
+ typeof index !== "number" ||
+ !Number.isInteger(index) ||
+ match[WEBHOOK_FIRST_ITEM_INDEX].length === WEBHOOK_FIRST_ITEM_INDEX
+ )
+ return null;
+ while (regions[regionIndex] && regions[regionIndex].end <= index)
+ regionIndex += WEBHOOK_REGION_STEP_COUNT;
+ const region = regions[regionIndex];
+ if (
+ !region ||
+ region.start > index ||
+ region.end < index + match[WEBHOOK_FIRST_ITEM_INDEX].length
+ )
+ return null;
+ if (region.isRetained && firstRetainedIndex === null) firstRetainedIndex = index;
+ }
+ return { index: firstRetainedIndex };
+ } catch {
+ return null;
+ }
+};
diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/webhook-entrypoint/constants.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/webhook-entrypoint/constants.ts
new file mode 100644
index 0000000000..aa9320201a
--- /dev/null
+++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/webhook-entrypoint/constants.ts
@@ -0,0 +1,13 @@
+export const WEBHOOK_EMPTY_FINDING_COUNT = 0;
+export const WEBHOOK_REGION_STEP_COUNT = 1;
+export const WEBHOOK_FIRST_ITEM_INDEX = 0;
+export const WEBHOOK_MARKER_CHAR_COUNT = 1;
+export const WEBHOOK_TEMPLATE_EXPRESSION_MARKER_CHAR_COUNT = 2;
+export const WEBHOOK_NON_ASCII_PATTERN = /\P{ASCII}/u;
+export const WEBHOOK_LITERAL_WHITESPACE_PATTERN = /\s/u;
+export const WEBHOOK_IDENTIFIER_NAME_PATTERN = /^(?:webhookHandler|webhookRoute)$/i;
+export const WEBHOOK_FUNCTION_NAME_PATTERN = /^POST$/i;
+export const WEBHOOK_VARIABLE_NAME_PATTERN = /^(?:POST|handler|webhook)$/i;
+export const WEBHOOK_SYNC_EXPORT_PATTERN = /^export\s+function\s+POST$/i;
+export const WEBHOOK_ASYNC_EXPORT_PATTERN = /^export\s+async\s+function\s+POST$/i;
+export const WEBHOOK_CONST_EXPORT_PATTERN = /^export\s+const\s+(?:POST|handler|webhook)$/i;
diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.entrypoint-position.performance.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.entrypoint-position.performance.test.ts
new file mode 100644
index 0000000000..8cf2d5488b
--- /dev/null
+++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.entrypoint-position.performance.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, it, vi } from "vite-plus/test";
+import * as sourceParser from "../../utils/parse-source-file.js";
+import { getLocationAtIndex } from "./utils/get-location-at-index.js";
+import { webhookSignatureRisk } from "./webhook-signature-risk.js";
+import {
+ WEBHOOK_POSITION_PROOF_COUNTS,
+ WEBHOOK_POSITION_PROOF_PARSE_CALL_COUNT,
+ WEBHOOK_POSITION_PROOF_QUOTED_SLICE_BASE_COUNT,
+ WEBHOOK_POSITION_PROOF_QUOTED_SLICES_PER_ENTRY,
+ WEBHOOK_POSITION_PROOF_TEMPLATE_SLICES_PER_ENTRY,
+ WEBHOOK_POSITION_PROOF_NESTED_SLICE_COUNT,
+} from "../../../test-utils/webhook-position-proof/constants.js";
+
+const shapes = [
+ "quoted-prose-before-runtime",
+ "template-quasis-and-runtime",
+ "nested-call-source-spans",
+];
+
+describe("webhook entrypoint cold source views", () => {
+ for (const shape of shapes) {
+ for (const count of WEBHOOK_POSITION_PROOF_COUNTS) {
+ it(`${shape} with ${count} entries`, () => {
+ const declarations = Array.from({ length: count }, (_, entryIndex) =>
+ shape === "quoted-prose-before-runtime"
+ ? `const text${entryIndex}="webhookHandler prose ${entryIndex}";`
+ : `const text${entryIndex}=\`webhookHandler prose \${webhookRoute(req)}\`;`,
+ ).join("\n");
+ let code = `const req={};\n${declarations}${shape === "quoted-prose-before-runtime" ? "\nwebhookRoute(req);" : ""}\n`;
+ if (shape === "nested-call-source-spans")
+ code = `const req={};\nconst output=${"wrap(".repeat(count)}"webhookHandler prose"${")".repeat(count)};\nwebhookRoute(req);\n`;
+ const controlCode =
+ shape === "template-quasis-and-runtime"
+ ? code.replaceAll("${webhookRoute(req)}", "")
+ : code.replace("\nwebhookRoute(req);", "");
+ const file = {
+ absolutePath: `/private/synthetic-entrypoints/server/webhooks/${shape}-${count}.js`,
+ relativePath: `server/webhooks/${shape}-${count}.js`,
+ content: code,
+ isGeneratedBundle: false,
+ };
+ const expectedLocation = getLocationAtIndex(code, code.indexOf("webhookRoute"));
+ const sourceParserSpy = vi.spyOn(sourceParser, "parseSourceText");
+ const sourceSliceSpy = vi.spyOn(String.prototype, "slice");
+ try {
+ const findings = webhookSignatureRisk.scan?.(file);
+ const sourceSliceCalls = sourceSliceSpy.mock.contexts.filter(
+ (context) => String(context) === code,
+ ).length;
+ let sliceLimit = WEBHOOK_POSITION_PROOF_NESTED_SLICE_COUNT;
+ if (shape === "quoted-prose-before-runtime")
+ sliceLimit =
+ count * WEBHOOK_POSITION_PROOF_QUOTED_SLICES_PER_ENTRY +
+ WEBHOOK_POSITION_PROOF_QUOTED_SLICE_BASE_COUNT;
+ else if (shape === "template-quasis-and-runtime")
+ sliceLimit = count * WEBHOOK_POSITION_PROOF_TEMPLATE_SLICES_PER_ENTRY;
+ expect(sourceParserSpy).toHaveBeenCalledTimes(WEBHOOK_POSITION_PROOF_PARSE_CALL_COUNT);
+ expect(
+ sourceParserSpy.mock.results.every(
+ (result) => result.type === "return" && result.value !== null,
+ ),
+ ).toBe(true);
+ expect(sourceSliceCalls).toBeLessThanOrEqual(sliceLimit);
+ expect(findings).toEqual([
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ ...expectedLocation,
+ },
+ ]);
+ } finally {
+ sourceSliceSpy.mockRestore();
+ sourceParserSpy.mockRestore();
+ }
+ expect(webhookSignatureRisk.scan?.({ ...file, content: controlCode })).toEqual([]);
+ });
+ }
+ }
+});
diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.entrypoint-position.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.entrypoint-position.test.ts
new file mode 100644
index 0000000000..2f32affdf4
--- /dev/null
+++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.entrypoint-position.test.ts
@@ -0,0 +1,1243 @@
+import { describe, expect, it, vi } from "vite-plus/test";
+import * as sourceParser from "../../utils/parse-source-file.js";
+import { CROSS_FILE_PARSE_MAX_BYTES } from "../../constants/thresholds.js";
+import { webhookSignatureRisk } from "./webhook-signature-risk.js";
+
+const cases = [
+ {
+ name: "legacy-proposal-double-quoted-prose-only",
+ code: 'export const makeSource = () => "const payload = req.body;";\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/generation/webhook-double-quoted-prose-only.js",
+ absolutePath: "/src/generation/webhook-double-quoted-prose-only.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-single-quoted-prose-only",
+ code: "export const makeSource = () => \"const payload = req.body;\";\nconsole.error('Could not write webhookRoute output');\n",
+ options: {
+ relativePath: "src/generation/webhook-single-quoted-prose-only.js",
+ absolutePath: "/src/generation/webhook-single-quoted-prose-only.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 32,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-template-quasi-prose-only",
+ code: 'export const makeSource = () => "const payload = req.body;";\nconsole.error(`Could not write webhookHandler output`);\n',
+ options: {
+ relativePath: "src/generation/webhook-template-quasi-prose-only.js",
+ absolutePath: "/src/generation/webhook-template-quasi-prose-only.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 32,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-generator-nested-template",
+ code: 'export const makeSource = (name) => `const req = input; await ${name}(req.body);`;\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/generation/webhook-generator-nested-template.js",
+ absolutePath: "/src/generation/webhook-generator-nested-template.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-literal-mixed-with-actual-post",
+ code: 'console.error("Error writing to webhookHandler output:");\nexport async function POST(request) { return request.body; }\n',
+ options: {
+ relativePath: "src/generation/webhook-literal-mixed-with-actual-post.js",
+ absolutePath: "/src/generation/webhook-literal-mixed-with-actual-post.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 2,
+ column: 1,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-actual-before-prose",
+ code: 'export async function POST(request) { return request.body; }\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/generation/webhook-actual-before-prose.js",
+ absolutePath: "/src/generation/webhook-actual-before-prose.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 1,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 1,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-template-interpolation-actual-reference",
+ code: "const output = `rendered ${webhookHandler(req)}`;\n",
+ options: {
+ relativePath: "src/generation/webhook-template-interpolation-actual-reference.js",
+ absolutePath: "/src/generation/webhook-template-interpolation-actual-reference.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 28,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-nested-interpolation-actual-reference",
+ code: "const output = `outer ${`inner ${webhookHandler(req)}`}`;\n",
+ options: {
+ relativePath: "src/generation/webhook-nested-interpolation-actual-reference.js",
+ absolutePath: "/src/generation/webhook-nested-interpolation-actual-reference.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 34,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 34,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-escaped-interpolation-is-text",
+ code: "const output = `generated \\${webhookHandler(req)} output`;\n",
+ options: {
+ relativePath: "src/generation/webhook-escaped-interpolation-is-text.js",
+ absolutePath: "/src/generation/webhook-escaped-interpolation-is-text.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 30,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-escaped-quote-prose",
+ code: 'const output = "escaped \\" webhookHandler payload req";\n',
+ options: {
+ relativePath: "src/generation/webhook-escaped-quote-prose.js",
+ absolutePath: "/src/generation/webhook-escaped-quote-prose.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 28,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-quoted-header-suppression-preserved",
+ code: 'export const webhookHandler = (req) => req.headers["x-signature"];\n',
+ options: {
+ relativePath: "src/generation/webhook-quoted-header-suppression-preserved.js",
+ absolutePath: "/src/generation/webhook-quoted-header-suppression-preserved.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [],
+ },
+ {
+ name: "legacy-proposal-helper-name-veto-preserved",
+ code: "export const webhookHandler = (req) => { verifySignature(req); return req.body; };\n",
+ options: {
+ relativePath: "src/generation/webhook-helper-name-veto-preserved.js",
+ absolutePath: "/src/generation/webhook-helper-name-veto-preserved.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [],
+ },
+ {
+ name: "legacy-proposal-actual-with-request-only-string",
+ code: 'export const webhookHandler = () => "request data is a generated note";\n',
+ options: {
+ relativePath: "src/generation/webhook-actual-with-request-only-string.js",
+ absolutePath: "/src/generation/webhook-actual-with-request-only-string.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 14,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 14,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-single-token-string-boundary",
+ code: 'const output = "webhookHandler"; const label = "req";\n',
+ options: {
+ relativePath: "src/generation/webhook-single-token-string-boundary.js",
+ absolutePath: "/src/generation/webhook-single-token-string-boundary.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 17,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 17,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-type-name-boundary",
+ code: 'interface webhookHandler {}\nconst text = "req";\n',
+ options: {
+ relativePath: "src/generation/webhook-type-name.ts",
+ absolutePath: "/src/generation/webhook-type-name.ts",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 11,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 11,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-literal-reference-no-prose",
+ code: 'const entrypoint = "webhookRoute"; const label = "req";\n',
+ options: {
+ relativePath: "src/generation/webhook-literal-reference-no-prose.js",
+ absolutePath: "/src/generation/webhook-literal-reference-no-prose.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 21,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 21,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-line-comment-fake-actual",
+ code: '// export async function POST(request) { return request.body; }\nexport const makeSource = () => "const payload = req.body;";\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/generation/webhook-line-comment-fake-actual.js",
+ absolutePath: "/src/generation/webhook-line-comment-fake-actual.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 3,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-block-comment-fake-actual",
+ code: '/* webhookHandler(req) */\nexport const makeSource = () => "const payload = req.body;";\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/generation/webhook-block-comment-fake-actual.js",
+ absolutePath: "/src/generation/webhook-block-comment-fake-actual.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 3,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-division-next-to-real-entrypoint",
+ code: "const \u5408\u8a08 = 8 / 2;\nexport const webhookHandler = (req) => req.body / \u5408\u8a08;\n",
+ options: {
+ relativePath: "src/generation/webhook-division-next-to-real-entrypoint.js",
+ absolutePath: "/src/generation/webhook-division-next-to-real-entrypoint.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 2,
+ column: 14,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 14,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-regexp-body-then-real-entrypoint",
+ code: "const words = /webhookHandler req/;\nexport const webhookHandler = (req) => req.body;\n",
+ options: {
+ relativePath: "src/generation/webhook-regexp-body-then-real-entrypoint.js",
+ absolutePath: "/src/generation/webhook-regexp-body-then-real-entrypoint.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [
+ {
+ line: 2,
+ column: 14,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 16,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-tagged-template-boundary",
+ code: "const code = compile`webhookHandler req`;\n",
+ options: {
+ relativePath: "src/generation/webhook-tagged-template-boundary.js",
+ absolutePath: "/src/generation/webhook-tagged-template-boundary.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 22,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-opaque-generated-text-transport",
+ code: 'const code = "export const webhookHandler = req => req.body";\nconsumeGeneratedSource(code);\n',
+ options: {
+ relativePath: "src/generation/webhook-opaque-generated-text-transport.js",
+ absolutePath: "/src/generation/webhook-opaque-generated-text-transport.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-same-code-different-production-path",
+ code: 'export const makeSource = () => "const payload = req.body;";\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/runtime/webhook-routes.js",
+ absolutePath: "/src/runtime/webhook-routes.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "legacy-proposal-generated-file-metadata-boundary",
+ code: 'export const makeSource = () => "const payload = req.body;";\nconsole.error("Error writing to webhookHandler output:");\n',
+ options: {
+ relativePath: "src/generation/webhook-generated-file-metadata-boundary.js",
+ absolutePath: "/src/generation/webhook-generated-file-metadata-boundary.js",
+ isGeneratedBundle: false,
+ execution: "scan",
+ reactProfileNotConsultedByDirectScan: true,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 33,
+ },
+ ],
+ },
+ {
+ name: "ast-position-contextual-of-division",
+ code: "const req={};const of=8;const output=of / webhookHandler(req) /2;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-00.js",
+ relativePath: "src/webhooks/case-00.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 43,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ name: "ast-position-spaced-property-keyword",
+ code: "const req={};const object={return:8};const output=object. return / webhookHandler(req) /2;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-01.js",
+ relativePath: "src/webhooks/case-01.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 68,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 68,
+ },
+ ],
+ },
+ {
+ name: "ast-position-repeated-ts-nonnull",
+ code: "const req={};declare const value:number;const output=value!! / webhookHandler(req) /2;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-02.ts",
+ relativePath: "src/webhooks/case-02.ts",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 64,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 64,
+ },
+ ],
+ },
+ {
+ name: "ast-position-astral-division-unchanged",
+ code: "const req={};const \ud801\udc00=8;const output=\ud801\udc00 / webhookHandler(req) /2;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-03.js",
+ relativePath: "src/webhooks/case-03.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 43,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ name: "ast-position-escaped-identifier-keyword-suffix",
+ code: "const req={};const \\u{10400}return=8;const output=\\u{10400}return / webhookHandler(req) /2;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-04.js",
+ relativePath: "src/webhooks/case-04.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 69,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 69,
+ },
+ ],
+ },
+ {
+ name: "ast-position-prose-only",
+ code: 'const req={};console.error("Error writing webhookHandler file",req);\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-05.js",
+ relativePath: "src/webhooks/case-05.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ name: "ast-position-prose-before-real",
+ code: 'const req={};console.error("webhookHandler prose");webhookHandler(req);\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-06.js",
+ relativePath: "src/webhooks/case-06.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 52,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 29,
+ },
+ ],
+ },
+ {
+ name: "ast-position-regex-before-real",
+ code: "const req={};const pattern=/webhookHandler(req)/;webhookRoute(req);\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-07.js",
+ relativePath: "src/webhooks/case-07.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 50,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 29,
+ },
+ ],
+ },
+ {
+ name: "ast-position-comment-before-real",
+ code: "const req={};/* webhookHandler prose */webhookRoute(req);\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-08.js",
+ relativePath: "src/webhooks/case-08.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 40,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 40,
+ },
+ ],
+ },
+ {
+ name: "ast-position-template-expression",
+ code: "const req={};const output=`webhookHandler prose ${webhookRoute(req)}`;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-09.js",
+ relativePath: "src/webhooks/case-09.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 51,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ name: "ast-position-tagged-template-quasi",
+ code: "const req={};const output=tag`webhookHandler prose`;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-10.js",
+ relativePath: "src/webhooks/case-10.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 31,
+ },
+ ],
+ },
+ {
+ name: "ast-position-single-token-literal",
+ code: 'const req={};const kind="webhookHandler";\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-11.js",
+ relativePath: "src/webhooks/case-11.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 26,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ name: "ast-position-single-token-specifier",
+ code: 'import "webhookHandler";const req={};\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-12.js",
+ relativePath: "src/webhooks/case-12.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 9,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 9,
+ },
+ ],
+ },
+ {
+ name: "ast-position-escaped-space-literal",
+ code: 'const req={};const text="prefix\\ webhookHandler";\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-13.js",
+ relativePath: "src/webhooks/case-13.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 34,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 34,
+ },
+ ],
+ },
+ {
+ name: "ast-position-escaped-quote-literal",
+ code: 'const req={};const text="prefix\\" webhookHandler prose";\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-14.js",
+ relativePath: "src/webhooks/case-14.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 35,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 35,
+ },
+ ],
+ },
+ {
+ name: "ast-position-type-alias-retention",
+ code: "const req={};type webhookHandler = string;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-15.ts",
+ relativePath: "src/webhooks/case-15.ts",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 19,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 19,
+ },
+ ],
+ },
+ {
+ name: "ast-position-pruned-annotation-fallback",
+ code: "const req={};declare const value:webhookHandler;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-16.ts",
+ relativePath: "src/webhooks/case-16.ts",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 34,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 34,
+ },
+ ],
+ },
+ {
+ name: "ast-position-pruned-return-fallback",
+ code: "const req={};const read=():webhookHandler=>req;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-17.ts",
+ relativePath: "src/webhooks/case-17.ts",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 28,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ name: "ast-position-unknown-after-prose",
+ code: 'const req={};console.error("webhookHandler prose");declare const value:webhookRoute;\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-18.ts",
+ relativePath: "src/webhooks/case-18.ts",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 29,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 29,
+ },
+ ],
+ },
+ {
+ name: "ast-position-export-post",
+ code: "export async function POST(req){return req;}\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-19.js",
+ relativePath: "src/webhooks/case-19.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 1,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 1,
+ },
+ ],
+ },
+ {
+ name: "ast-position-export-const-handler",
+ code: "export const handler=(req)=>req;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-20.js",
+ relativePath: "src/webhooks/case-20.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 1,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 1,
+ },
+ ],
+ },
+ {
+ name: "ast-position-export-comments",
+ code: "export /* note */ const POST=(req)=>req;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-21.js",
+ relativePath: "src/webhooks/case-21.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 1,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 1,
+ },
+ ],
+ },
+ {
+ name: "ast-position-fake-export-prose",
+ code: 'const req={};const text="export const POST prose";\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-22.js",
+ relativePath: "src/webhooks/case-22.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ name: "ast-position-parse-failure-preserve",
+ code: 'const req={};const broken=;console.error("webhookHandler prose");\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-23.js",
+ relativePath: "src/webhooks/case-23.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 43,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 43,
+ },
+ ],
+ },
+ {
+ name: "ast-position-private-name-unknown",
+ code: "const req={};class Store {#webhookHandler=1;read(){return this.#webhookHandler;}}\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-24.js",
+ relativePath: "src/webhooks/case-24.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 28,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 28,
+ },
+ ],
+ },
+ {
+ name: "ast-position-crlf-code-location",
+ code: 'const req={};\r\nconsole.error("webhookHandler prose");\r\nwebhookRoute(req);\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-25.js",
+ relativePath: "src/webhooks/case-25.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 3,
+ column: 1,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 2,
+ column: 16,
+ },
+ ],
+ },
+ {
+ name: "ast-position-unescaped-unicode-data-fallback",
+ code: 'const req={};console.error("caf\u00e9 webhookHandler prose");\n',
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-26.js",
+ relativePath: "src/webhooks/case-26.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 34,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 34,
+ },
+ ],
+ },
+ {
+ name: "ast-position-single-token-template",
+ code: "const req={};const text=`webhookHandler`;\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-27.js",
+ relativePath: "src/webhooks/case-27.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ name: "ast-position-regex-nested-character-class",
+ code: "const req={};const pattern=/[webhookHandler/]/;webhookRoute(req);\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-28.js",
+ relativePath: "src/webhooks/case-28.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 48,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 30,
+ },
+ ],
+ },
+ {
+ name: "ast-position-member-property-identifier",
+ code: "const req={};object.webhookHandler(req);\n",
+ options: {
+ absolutePath: "/private/react-doctor-synthetic/ast-entrypoint/src/webhooks/case-29.js",
+ relativePath: "src/webhooks/case-29.js",
+ isGeneratedBundle: false,
+ },
+ expectedLocations: [
+ {
+ line: 1,
+ column: 21,
+ },
+ ],
+ legacyFindings: [
+ {
+ message: "Webhook handler code does not show an obvious signature verification step.",
+ line: 1,
+ column: 21,
+ },
+ ],
+ },
+];
+
+describe("AST classified entrypoint positions", () => {
+ it.each(cases)("$name", ({ code, options, expectedLocations }) => {
+ const findings = webhookSignatureRisk.scan?.({ ...options, content: code });
+ expect(findings?.map(({ line, column }) => ({ line, column }))).toEqual(expectedLocations);
+ });
+
+ it("retains the complete legacy finding when parsing returns null", () => {
+ const [scenario] = cases;
+ if (!scenario) throw new Error("Missing synthetic scenario");
+ const parser = vi.spyOn(sourceParser, "parseSourceText").mockReturnValueOnce(null);
+ try {
+ expect(webhookSignatureRisk.scan?.({ ...scenario.options, content: scenario.code })).toEqual(
+ scenario.legacyFindings,
+ );
+ expect(parser).toHaveBeenCalledWith({
+ filename: scenario.options.absolutePath,
+ sourceText: scenario.code,
+ shouldAttachParentReferences: false,
+ });
+ } finally {
+ parser.mockRestore();
+ }
+ });
+
+ it("retains the complete legacy finding when parsing throws", () => {
+ const [scenario] = cases;
+ if (!scenario) throw new Error("Missing synthetic scenario");
+ const parser = vi.spyOn(sourceParser, "parseSourceText").mockImplementationOnce(() => {
+ throw new Error("Parser unavailable");
+ });
+ try {
+ expect(webhookSignatureRisk.scan?.({ ...scenario.options, content: scenario.code })).toEqual(
+ scenario.legacyFindings,
+ );
+ } finally {
+ parser.mockRestore();
+ }
+ });
+
+ it("does not parse beyond the existing source byte budget", () => {
+ const [scenario] = cases;
+ if (!scenario) throw new Error("Missing synthetic scenario");
+ const parser = vi.spyOn(sourceParser, "parseSourceText");
+ try {
+ expect(
+ webhookSignatureRisk.scan?.({
+ ...scenario.options,
+ content: scenario.code + " ".repeat(CROSS_FILE_PARSE_MAX_BYTES),
+ }),
+ ).toEqual(scenario.legacyFindings);
+ expect(parser).not.toHaveBeenCalled();
+ } finally {
+ parser.mockRestore();
+ }
+ });
+
+ it("does not parse non ASCII source", () => {
+ const scenario = cases.find(
+ (entry) => entry.name === "legacy-proposal-division-next-to-real-entrypoint",
+ );
+ if (!scenario) throw new Error("Missing synthetic scenario");
+ const parser = vi.spyOn(sourceParser, "parseSourceText");
+ try {
+ expect(webhookSignatureRisk.scan?.({ ...scenario.options, content: scenario.code })).toEqual(
+ scenario.legacyFindings,
+ );
+ expect(parser).not.toHaveBeenCalled();
+ } finally {
+ parser.mockRestore();
+ }
+ });
+
+ it("returns an old quiet gate without parsing", () => {
+ const scenario = cases.find(
+ (entry) => entry.name === "legacy-proposal-quoted-header-suppression-preserved",
+ );
+ if (!scenario) throw new Error("Missing synthetic scenario");
+ const parser = vi.spyOn(sourceParser, "parseSourceText");
+ try {
+ expect(webhookSignatureRisk.scan?.({ ...scenario.options, content: scenario.code })).toEqual(
+ scenario.legacyFindings,
+ );
+ expect(parser).not.toHaveBeenCalled();
+ } finally {
+ parser.mockRestore();
+ }
+ });
+
+ it("retains the original whole array when a later match is unclassified", () => {
+ const scenario = cases.find((entry) => entry.name === "ast-position-unknown-after-prose");
+ if (!scenario) throw new Error("Missing synthetic scenario");
+ expect(webhookSignatureRisk.scan?.({ ...scenario.options, content: scenario.code })).toEqual(
+ scenario.legacyFindings,
+ );
+ });
+});
diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.ts
index f482b9bb66..1eefbd6825 100644
--- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.ts
+++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/webhook-signature-risk.ts
@@ -1,3 +1,6 @@
+import { getClassifiedWebhookEntrypoint } from "./utils/get-classified-webhook-entrypoint.js";
+import { getLocationAtIndex } from "./utils/get-location-at-index.js";
+import { WEBHOOK_EMPTY_FINDING_COUNT } from "./utils/webhook-entrypoint/constants.js";
import { defineRule } from "../../utils/define-rule.js";
import { isProductionSourcePath } from "./utils/is-production-source-path.js";
import { scanByPattern } from "./utils/scan-by-pattern.js";
@@ -49,27 +52,37 @@ const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
const COMMENT_OR_STRING_PATTERN =
/\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g;
+const scanWebhookEntrypoints = scanByPattern({
+ shouldScan: (file) => {
+ if (!isProductionSourcePath(file.relativePath)) return false;
+ if (OUTBOUND_WEBHOOK_CONFIG_PATTERN.test(file.content)) return false;
+ const judgeableContent = file.content
+ .replace(COMMENT_OR_STRING_PATTERN, "")
+ .replace(OUTBOUND_WEBHOOK_URL_MENTION_PATTERN, "");
+ return (
+ WEBHOOK_HANDLER_PATTERN.test(file.relativePath) ||
+ WEBHOOK_HANDLER_PATTERN.test(judgeableContent)
+ );
+ },
+ pattern: WEBHOOK_ENTRYPOINT_PATTERN,
+ requireAll: [REQUEST_READ_PATTERN],
+ suppressWhen: WEBHOOK_SIGNATURE_VERIFICATION_PATTERN,
+ message: "Webhook handler code does not show an obvious signature verification step.",
+});
+
export const webhookSignatureRisk = defineRule({
id: "webhook-signature-risk",
title: "Webhook handler lacks signature verification",
severity: "warn",
recommendation:
"Verify provider signatures before parsing or acting on webhook bodies. Use provider SDK helpers or HMAC verification with timing-safe comparison.",
- scan: scanByPattern({
- shouldScan: (file) => {
- if (!isProductionSourcePath(file.relativePath)) return false;
- if (OUTBOUND_WEBHOOK_CONFIG_PATTERN.test(file.content)) return false;
- const judgeableContent = file.content
- .replace(COMMENT_OR_STRING_PATTERN, "")
- .replace(OUTBOUND_WEBHOOK_URL_MENTION_PATTERN, "");
- return (
- WEBHOOK_HANDLER_PATTERN.test(file.relativePath) ||
- WEBHOOK_HANDLER_PATTERN.test(judgeableContent)
- );
- },
- pattern: WEBHOOK_ENTRYPOINT_PATTERN,
- requireAll: [REQUEST_READ_PATTERN],
- suppressWhen: WEBHOOK_SIGNATURE_VERIFICATION_PATTERN,
- message: "Webhook handler code does not show an obvious signature verification step.",
- }),
+ scan: (file) => {
+ const findings = scanWebhookEntrypoints(file);
+ if (findings.length === WEBHOOK_EMPTY_FINDING_COUNT) return findings;
+ const entrypoint = getClassifiedWebhookEntrypoint(file, WEBHOOK_ENTRYPOINT_PATTERN);
+ if (!entrypoint) return findings;
+ if (entrypoint.index === null) return [];
+ const location = getLocationAtIndex(file.content, entrypoint.index);
+ return findings.map((finding) => ({ ...finding, ...location }));
+ },
});
diff --git a/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-mutate-then-set-or-return-same-reference.test.ts b/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-mutate-then-set-or-return-same-reference.test.ts
index c328a49d12..93e63c999c 100644
--- a/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-mutate-then-set-or-return-same-reference.test.ts
+++ b/packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-mutate-then-set-or-return-same-reference.test.ts
@@ -1,8 +1,37 @@
-import { describe, expect, it } from "vite-plus/test";
+import { describe, expect, it, vi } from "vite-plus/test";
import { runRule } from "../../../test-utils/run-rule.js";
import { noMutateThenSetOrReturnSameReference } from "./no-mutate-then-set-or-return-same-reference.js";
-const MEASUREMENT_SAMPLE_COUNT = 3;
+interface PairingStressFixture {
+ readonly source: string;
+ readonly pairingSiteBudget: number;
+}
+
+interface PairingWorkMeasurement {
+ readonly coexistenceCalls: number;
+ readonly reachabilityCalls: number;
+}
+
+const coexistenceCounter = vi.hoisted(() => vi.fn());
+const reachabilityCounter = vi.hoisted(() => vi.fn());
+
+vi.mock("../../utils/nodes-can-co-execute.js", async (importOriginal) => {
+ const originalModule =
+ await importOriginal();
+ return {
+ ...originalModule,
+ nodesCanCoExecute: coexistenceCounter.mockImplementation(originalModule.nodesCanCoExecute),
+ };
+});
+
+vi.mock("../../utils/can-node-reach-node.js", async (importOriginal) => {
+ const originalModule =
+ await importOriginal();
+ return {
+ ...originalModule,
+ canNodeReachNode: reachabilityCounter.mockImplementation(originalModule.canNodeReachNode),
+ };
+});
describe("no-mutate-then-set-or-return-same-reference", () => {
it("flags setX(state.add(index)) on a state Set", () => {
@@ -561,7 +590,7 @@ describe("no-mutate-then-set-or-return-same-reference", () => {
});
it("amortizes mutation and same-reference pairing by control-flow block", () => {
- const buildSource = (pairCount: number): string => {
+ const buildSource = (pairCount: number): PairingStressFixture => {
const mutations = "items.push(1);".repeat(pairCount);
const conditions = Array.from(
{ length: pairCount },
@@ -579,21 +608,48 @@ describe("no-mutate-then-set-or-return-same-reference", () => {
return `(${buildBalancedResultExpression(startIndex, middleIndex)}||${buildBalancedResultExpression(middleIndex, endIndex)})`;
};
const sameReferenceResults = buildBalancedResultExpression(0, resultExpressions.length);
- return `const C=({flag,${conditions.join(",")}})=>{const[,setItems]=useState([]);setItems(items=>{if(flag){${mutations}}if(!flag){return ${sameReferenceResults}}return [...items]})}`;
+ const mutationRegions = [`if(flag){${mutations}}`];
+ const returnSites = [`if(!flag){return ${sameReferenceResults}}`, "return [...items]"];
+ return {
+ source: `const C=({flag,${conditions.join(",")}})=>{const[,setItems]=useState([]);setItems(items=>{${mutationRegions.join("")}${returnSites.join("")}})}`,
+ pairingSiteBudget: mutationRegions.length * returnSites.length,
+ };
};
- runRule(noMutateThenSetOrReturnSameReference, buildSource(200));
- const measureFastestDuration = (pairCount: number): number => {
- let fastestDuration = Number.POSITIVE_INFINITY;
- for (let repetition = 0; repetition < MEASUREMENT_SAMPLE_COUNT; repetition += 1) {
- const startedAt = performance.now();
- const result = runRule(noMutateThenSetOrReturnSameReference, buildSource(pairCount));
- fastestDuration = Math.min(fastestDuration, performance.now() - startedAt);
- expect(result.diagnostics).toHaveLength(0);
- }
- return fastestDuration;
+ const measurePairingWork = (
+ source: string,
+ expectedDiagnosticCount: number,
+ ): PairingWorkMeasurement => {
+ coexistenceCounter.mockClear();
+ reachabilityCounter.mockClear();
+ const result = runRule(noMutateThenSetOrReturnSameReference, source);
+ expect(result.parseErrors).toHaveLength(0);
+ expect(result.diagnostics).toHaveLength(expectedDiagnosticCount);
+ return {
+ coexistenceCalls: coexistenceCounter.mock.calls.length,
+ reachabilityCalls: reachabilityCounter.mock.calls.length,
+ };
};
- const smallDuration = measureFastestDuration(800);
- const largeDuration = measureFastestDuration(4_000);
- expect(largeDuration).toBeLessThan(smallDuration * 18);
+ const positive = measurePairingWork(
+ "const C=()=>{const[,setItems]=useState([]);setItems(items=>{items.push(1);return items})}",
+ 1,
+ );
+ expect(positive.coexistenceCalls).toBeGreaterThan(0);
+ expect(positive.reachabilityCalls).toBeGreaterThan(0);
+
+ const smallFixture = buildSource(800);
+ const small = measurePairingWork(smallFixture.source, 0);
+ expect(small.coexistenceCalls).toBeGreaterThan(0);
+ expect(small.coexistenceCalls).toBeLessThanOrEqual(smallFixture.pairingSiteBudget);
+ expect(small.reachabilityCalls).toBeGreaterThan(0);
+ expect(small.reachabilityCalls).toBeLessThanOrEqual(smallFixture.pairingSiteBudget);
+
+ const largeFixture = buildSource(4_000);
+ const large = measurePairingWork(largeFixture.source, 0);
+ expect(large.coexistenceCalls).toBeGreaterThan(0);
+ expect(large.coexistenceCalls).toBeLessThanOrEqual(largeFixture.pairingSiteBudget);
+ expect(large.reachabilityCalls).toBeGreaterThan(0);
+ expect(large.reachabilityCalls).toBeLessThanOrEqual(largeFixture.pairingSiteBudget);
+ expect(large.coexistenceCalls).toBe(small.coexistenceCalls);
+ expect(large.reachabilityCalls).toBe(small.reachabilityCalls);
});
});
diff --git a/packages/oxlint-plugin-react-doctor/src/test-utils/webhook-position-proof/constants.ts b/packages/oxlint-plugin-react-doctor/src/test-utils/webhook-position-proof/constants.ts
new file mode 100644
index 0000000000..d385c42e54
--- /dev/null
+++ b/packages/oxlint-plugin-react-doctor/src/test-utils/webhook-position-proof/constants.ts
@@ -0,0 +1,6 @@
+export const WEBHOOK_POSITION_PROOF_COUNTS = [20, 100];
+export const WEBHOOK_POSITION_PROOF_PARSE_CALL_COUNT = 1;
+export const WEBHOOK_POSITION_PROOF_QUOTED_SLICE_BASE_COUNT = 1;
+export const WEBHOOK_POSITION_PROOF_QUOTED_SLICES_PER_ENTRY = 2;
+export const WEBHOOK_POSITION_PROOF_TEMPLATE_SLICES_PER_ENTRY = 3;
+export const WEBHOOK_POSITION_PROOF_NESTED_SLICE_COUNT = 3;