diff --git a/.changeset/kind-onions-invent.md b/.changeset/kind-onions-invent.md new file mode 100644 index 0000000..85c42db --- /dev/null +++ b/.changeset/kind-onions-invent.md @@ -0,0 +1,5 @@ +--- +'dotenv-diff': patch +--- + +improved source secret detection for JSX props diff --git a/packages/cli/src/core/security/secretDetectors.ts b/packages/cli/src/core/security/secretDetectors.ts index 0f3808f..e27ff9e 100644 --- a/packages/cli/src/core/security/secretDetectors.ts +++ b/packages/cli/src/core/security/secretDetectors.ts @@ -52,6 +52,19 @@ const HARMLESS_URLS = [ /xmlns=["']http:\/\/www\.w3\.org\/2000\/svg["']/i, // SVG namespace ]; +// Known harmless attribute keys commonly used in UI components +const HARMLESS_UI_ATTRIBUTE_NAMES = + /^(name|label|placeholder|title|alt|caption|helperText|description|text|htmlFor|id|data-testid|data-test|aria-label)$/i; + +/** + * Checks if a string looks like a UI label or attribute value, which are often false positives in secret detection. + * @param s - The string to check. + * @returns True if the string looks like a UI label, false otherwise. + */ +function looksLikeUiLabel(s: string): boolean { + return /\s/.test(s); +} + // Known harmless attribute keys commonly used in UI / analytics const HARMLESS_ATTRIBUTE_KEYS = /\b(trackingId|trackingContext|data-testid|data-test|aria-label)\b/i; @@ -318,15 +331,26 @@ export function detectSecretsInSource( // Ignore if inside HTML tag content if (/<[^>]*>.*<\/[^>]*>/.test(line.trim())) continue; - const m = line.match(/=\s*["'`](.+?)["'`]/); + const attrMatch = line.match( + /([:@A-Za-z0-9_-]+)\s*=\s*(?:\{\s*["'`](.+?)["'`]\s*\}|["'`](.+?)["'`])/, + ); + + if (!attrMatch) continue; + + const attrName = attrMatch[1]; + const literal = attrMatch[2] ?? attrMatch[3]; + + // Skip common UI props like label, placeholder, name, etc. + if (HARMLESS_UI_ATTRIBUTE_NAMES.test(attrName!)) continue; + if ( - m && - m[1] && - !looksHarmlessLiteral(m[1]) && + literal && + !looksHarmlessLiteral(literal) && + !looksLikeUiLabel(literal) && !looksLikeUrlConstruction(line) && - m[1].length >= 12 && + literal.length >= 12 && !isEnvAccessor(line) && - !isPureInterpolationTemplate(m[1]) + !isPureInterpolationTemplate(literal) ) { findings.push({ file, diff --git a/packages/cli/test/unit/core/security/secretDetectors.test.ts b/packages/cli/test/unit/core/security/secretDetectors.test.ts index 3cbb961..74e84c3 100644 --- a/packages/cli/test/unit/core/security/secretDetectors.test.ts +++ b/packages/cli/test/unit/core/security/secretDetectors.test.ts @@ -457,6 +457,38 @@ const email = "user@example.com"; expect(findings).toHaveLength(0); }); + it('does not flag JSX label props containing password text', () => { + const source = ''; + expect(detectSecretsInSource('Component.tsx', source)).toEqual([]); + }); + + it('does not flag JSX placeholder props containing password text', () => { + const source = ''; + expect(detectSecretsInSource('Component.tsx', source)).toEqual([]); + }); + + it('does not flag JSX name props like currentPassword', () => { + const source = ''; + expect(detectSecretsInSource('Component.tsx', source)).toEqual([]); + }); + + it('does not flag JSX expression props without string literals', () => { + const source = ''; + expect(detectSecretsInSource('Component.tsx', source)).toEqual([]); + }); + + it('still flags hardcoded secret in JSX prop string literal', () => { + const source = ''; + const findings = detectSecretsInSource('Component.tsx', source); + expect(findings.length).toBeGreaterThan(0); + }); + + it('still flags hardcoded token in JSX prop expression string', () => { + const source = ''; + const findings = detectSecretsInSource('Component.tsx', source); + expect(findings.length).toBeGreaterThan(0); + }); + describe('charset and alphabet detection', () => { it('should ignore full alphanumeric alphabet (customAlphabet pattern)', () => { // The exact case from the bug report