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.
This commit is contained in:
Ben Senescu 2026-07-05 17:50:01 -04:00 committed by GitHub
parent 7c64cd3c5c
commit 9b6c5640cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -21,7 +21,12 @@ import {
type Report, type Report,
type SearchPerformanceTableRow, type SearchPerformanceTableRow,
} from "@/client/features/search-performance/SearchPerformanceColumns"; } 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 { getStandardErrorMessage } from "@/client/lib/error-messages";
import { exportTableToSheets } from "@/client/lib/exportToSheets"; import { exportTableToSheets } from "@/client/lib/exportToSheets";
import { captureClientEvent } from "@/client/lib/posthog"; import { captureClientEvent } from "@/client/lib/posthog";
@ -277,7 +282,12 @@ export function StrikingDistanceTable({
const copyKeywords = async () => { const copyKeywords = async () => {
try { 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( toast.success(
`Copied ${selectedQueries.length} ${selectedQueries.length === 1 ? "keyword" : "keywords"}`, `Copied ${selectedQueries.length} ${selectedQueries.length === 1 ? "keyword" : "keywords"}`,
); );