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