From 9b6c5640cde899c8b04130fcbfd1cc621076134e Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:50:01 -0400 Subject: [PATCH] Sanitize Copy keywords clipboard against spreadsheet formula injection (#348) The Striking Distance 'Copy keywords' action wrote raw selected GSC query strings to the clipboard, bypassing the formula-injection sanitizer used by the CSV and Sheets export paths. GSC query strings are untrusted and can begin with =, +, -, @, tab, CR, or LF; pasting them into Sheets/Excel could execute them as formulas. Route the copy path through normalizeExportValue (same OWASP-recommended guard as CSV/Sheets exports) so dangerous leading characters are prefixed with a single quote. --- .../search-performance/SearchPerformanceParts.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/client/features/search-performance/SearchPerformanceParts.tsx b/src/client/features/search-performance/SearchPerformanceParts.tsx index 2e541a1..e3eee89 100644 --- a/src/client/features/search-performance/SearchPerformanceParts.tsx +++ b/src/client/features/search-performance/SearchPerformanceParts.tsx @@ -21,7 +21,12 @@ import { type Report, type SearchPerformanceTableRow, } from "@/client/features/search-performance/SearchPerformanceColumns"; -import { buildCsv, downloadCsv, type CsvValue } from "@/client/lib/csv"; +import { + buildCsv, + downloadCsv, + normalizeExportValue, + type CsvValue, +} from "@/client/lib/csv"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { exportTableToSheets } from "@/client/lib/exportToSheets"; import { captureClientEvent } from "@/client/lib/posthog"; @@ -277,7 +282,12 @@ export function StrikingDistanceTable({ const copyKeywords = async () => { try { - await navigator.clipboard.writeText(selectedQueries.join("\n")); + // Sanitize against spreadsheet formula injection: GSC query strings are + // untrusted and may begin with =, +, -, @, etc. See @/client/lib/csv. + const text = selectedQueries + .map((query) => normalizeExportValue(query)) + .join("\n"); + await navigator.clipboard.writeText(text); toast.success( `Copied ${selectedQueries.length} ${selectedQueries.length === 1 ? "keyword" : "keywords"}`, );