Skip to content

Commit 95de9f0

Browse files
committed
🔒 security(ui): tighten vulnerability CSV export escaping
- Always quote every CSV field instead of only when it contains commas/quotes/newlines - Escape tab and carriage return as formula-injection leading characters in addition to =+-@ - Quote and escape column headers on the same code path as data cells
1 parent 981f7f8 commit 95de9f0

File tree

2 files changed

+25
-14
lines changed

2 files changed

+25
-14
lines changed

‎ui/src/views/security/securityViewUtils.ts‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,8 @@ export function buildSecurityEmptyState(input: SecurityViewEmptyStateInput): Sec
151151
export type VulnExportFormat = 'json' | 'csv';
152152

153153
function escapeCsvField(value: string): string {
154-
const sanitizedValue = /^[=+\-@]/.test(value) ? `'${value}` : value;
155-
if (
156-
sanitizedValue.includes(',') ||
157-
sanitizedValue.includes('"') ||
158-
sanitizedValue.includes('\n')
159-
) {
160-
return `"${sanitizedValue.replace(/"/g, '""')}"`;
161-
}
162-
return sanitizedValue;
154+
const sanitizedValue = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
155+
return `"${sanitizedValue.replace(/"/g, '""')}"`;
163156
}
164157

165158
const VULN_CSV_COLUMNS = [
@@ -174,7 +167,7 @@ const VULN_CSV_COLUMNS = [
174167
] as const;
175168

176169
export function vulnReportToCsv(vulns: Vulnerability[]): string {
177-
const rows = [VULN_CSV_COLUMNS.join(',')];
170+
const rows = [VULN_CSV_COLUMNS.map(escapeCsvField).join(',')];
178171
for (const v of vulns) {
179172
rows.push(
180173
[

‎ui/tests/views/security/securityViewUtils.spec.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ describe('securityViewUtils', () => {
218218
expect(toSafeExternalUrl('javascript:alert(1)')).toBeNull();
219219
expect(toSafeExternalUrl('ftp://example.com')).toBeNull();
220220
expect(toSafeExternalUrl('data:text/html,<h1>hi</h1>')).toBeNull();
221+
expect(toSafeExternalUrl('file:///etc/passwd')).toBeNull();
221222
});
222223

223224
it('returns null for invalid URLs', () => {
@@ -240,15 +241,17 @@ describe('securityViewUtils', () => {
240241
};
241242

242243
it('returns only header row for empty array', () => {
243-
expect(vulnReportToCsv([])).toBe('ID,Severity,Package,Version,Fixed In,Title,Target,URL');
244+
expect(vulnReportToCsv([])).toBe(
245+
'"ID","Severity","Package","Version","Fixed In","Title","Target","URL"',
246+
);
244247
});
245248

246249
it('formats a single vulnerability as CSV', () => {
247250
const csv = vulnReportToCsv([baseVuln]);
248251
const lines = csv.split('\n');
249252
expect(lines).toHaveLength(2);
250253
expect(lines[1]).toBe(
251-
'CVE-2026-1234,HIGH,openssl,1.1.1,1.1.2,Buffer overflow,usr/lib/libssl.so,https://nvd.nist.gov/vuln/detail/CVE-2026-1234',
254+
'"CVE-2026-1234","HIGH","openssl","1.1.1","1.1.2","Buffer overflow","usr/lib/libssl.so","https://nvd.nist.gov/vuln/detail/CVE-2026-1234"',
252255
);
253256
});
254257

@@ -262,7 +265,7 @@ describe('securityViewUtils', () => {
262265
};
263266
const csv = vulnReportToCsv([vuln]);
264267
const lines = csv.split('\n');
265-
expect(lines[1]).toBe('CVE-2026-1234,HIGH,openssl,1.1.1,,,,');
268+
expect(lines[1]).toBe('"CVE-2026-1234","HIGH","openssl","1.1.1","","","",""');
266269
});
267270

268271
it('escapes fields containing commas and quotes', () => {
@@ -290,7 +293,22 @@ describe('securityViewUtils', () => {
290293
const lines = csv.split('\n');
291294

292295
expect(lines[1]).toBe(
293-
"'=CVE-2026-1234,'+HIGH,'-openssl,'@1.1.1,1.1.2,Buffer overflow,usr/lib/libssl.so,https://nvd.nist.gov/vuln/detail/CVE-2026-1234",
296+
`"'=CVE-2026-1234","'+HIGH","'-openssl","'@1.1.1","1.1.2","Buffer overflow","usr/lib/libssl.so","https://nvd.nist.gov/vuln/detail/CVE-2026-1234"`,
297+
);
298+
});
299+
300+
it('prefixes tab-leading fields and escapes embedded quotes', () => {
301+
const vuln: Vulnerability = {
302+
...baseVuln,
303+
id: '\t=cmd',
304+
title: 'Buffer "overflow"',
305+
};
306+
307+
const csv = vulnReportToCsv([vuln]);
308+
const lines = csv.split('\n');
309+
310+
expect(lines[1]).toBe(
311+
'"\'\t=cmd","HIGH","openssl","1.1.1","1.1.2","Buffer ""overflow""","usr/lib/libssl.so","https://nvd.nist.gov/vuln/detail/CVE-2026-1234"',
294312
);
295313
});
296314
});

0 commit comments

Comments
 (0)