Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/kind-onions-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'dotenv-diff': patch
---

improved source secret detection for JSX props
36 changes: 30 additions & 6 deletions packages/cli/src/core/security/secretDetectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions packages/cli/test/unit/core/security/secretDetectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<Password label="Current password" />';
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
});

it('does not flag JSX placeholder props containing password text', () => {
const source = '<Input placeholder="Re-enter password" />';
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
});

it('does not flag JSX name props like currentPassword', () => {
const source = '<Password name="currentPassword" />';
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
});

it('does not flag JSX expression props without string literals', () => {
const source = '<Password passwordError={currentPasswordError} />';
expect(detectSecretsInSource('Component.tsx', source)).toEqual([]);
});

it('still flags hardcoded secret in JSX prop string literal', () => {
const source = '<SecretField value="sk_live_abcdefghijklmnopqrstuvwxyz123456" />';
const findings = detectSecretsInSource('Component.tsx', source);
expect(findings.length).toBeGreaterThan(0);
});

it('still flags hardcoded token in JSX prop expression string', () => {
const source = '<TokenField token={"ghp_abcdefghijklmnopqrstuvwxyz1234567890"} />';
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
Expand Down