Skip to content

Commit 3d47c38

Browse files
authored
fix: reduce false positives for JSX secret detection (#363)
1 parent 2869c10 commit 3d47c38

3 files changed

Lines changed: 67 additions & 6 deletions

File tree

.changeset/kind-onions-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dotenv-diff': patch
3+
---
4+
5+
improved source secret detection for JSX props

packages/cli/src/core/security/secretDetectors.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ const HARMLESS_URLS = [
5252
/xmlns=["']http:\/\/www\.w3\.org\/2000\/svg["']/i, // SVG namespace
5353
];
5454

55+
// Known harmless attribute keys commonly used in UI components
56+
const HARMLESS_UI_ATTRIBUTE_NAMES =
57+
/^(name|label|placeholder|title|alt|caption|helperText|description|text|htmlFor|id|data-testid|data-test|aria-label)$/i;
58+
59+
/**
60+
* Checks if a string looks like a UI label or attribute value, which are often false positives in secret detection.
61+
* @param s - The string to check.
62+
* @returns True if the string looks like a UI label, false otherwise.
63+
*/
64+
function looksLikeUiLabel(s: string): boolean {
65+
return /\s/.test(s);
66+
}
67+
5568
// Known harmless attribute keys commonly used in UI / analytics
5669
const HARMLESS_ATTRIBUTE_KEYS =
5770
/\b(trackingId|trackingContext|data-testid|data-test|aria-label)\b/i;
@@ -318,15 +331,26 @@ export function detectSecretsInSource(
318331
// Ignore if inside HTML tag content
319332
if (/<[^>]*>.*<\/[^>]*>/.test(line.trim())) continue;
320333

321-
const m = line.match(/=\s*["'`](.+?)["'`]/);
334+
const attrMatch = line.match(
335+
/([:@A-Za-z0-9_-]+)\s*=\s*(?:\{\s*["'`](.+?)["'`]\s*\}|["'`](.+?)["'`])/,
336+
);
337+
338+
if (!attrMatch) continue;
339+
340+
const attrName = attrMatch[1];
341+
const literal = attrMatch[2] ?? attrMatch[3];
342+
343+
// Skip common UI props like label, placeholder, name, etc.
344+
if (HARMLESS_UI_ATTRIBUTE_NAMES.test(attrName!)) continue;
345+
322346
if (
323-
m &&
324-
m[1] &&
325-
!looksHarmlessLiteral(m[1]) &&
347+
literal &&
348+
!looksHarmlessLiteral(literal) &&
349+
!looksLikeUiLabel(literal) &&
326350
!looksLikeUrlConstruction(line) &&
327-
m[1].length >= 12 &&
351+
literal.length >= 12 &&
328352
!isEnvAccessor(line) &&
329-
!isPureInterpolationTemplate(m[1])
353+
!isPureInterpolationTemplate(literal)
330354
) {
331355
findings.push({
332356
file,

packages/cli/test/unit/core/security/secretDetectors.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,38 @@ const email = "user@example.com";
455455
expect(findings).toHaveLength(0);
456456
});
457457

458+
it('does not flag JSX label props containing password text', () => {
459+
const source = '<Password label="Current password" />';
460+
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
461+
});
462+
463+
it('does not flag JSX placeholder props containing password text', () => {
464+
const source = '<Input placeholder="Re-enter password" />';
465+
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
466+
});
467+
468+
it('does not flag JSX name props like currentPassword', () => {
469+
const source = '<Password name="currentPassword" />';
470+
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
471+
});
472+
473+
it('does not flag JSX expression props without string literals', () => {
474+
const source = '<Password passwordError={currentPasswordError} />';
475+
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
476+
});
477+
478+
it('still flags hardcoded secret in JSX prop string literal', () => {
479+
const source = '<SecretField value="sk_live_abcdefghijklmnopqrstuvwxyz123456" />';
480+
const findings = detectSecretsInSource('Component.tsx', source);
481+
expect(findings.length).toBeGreaterThan(0);
482+
});
483+
484+
it('still flags hardcoded token in JSX prop expression string', () => {
485+
const source = '<TokenField token={"ghp_abcdefghijklmnopqrstuvwxyz1234567890"} />';
486+
const findings = detectSecretsInSource('Component.tsx', source);
487+
expect(findings.length).toBeGreaterThan(0);
488+
});
489+
458490
describe('charset and alphabet detection', () => {
459491
it('should ignore full alphanumeric alphabet (customAlphabet pattern)', () => {
460492
// The exact case from the bug report

0 commit comments

Comments
 (0)