Skip to content

Commit b08270b

Browse files
fix(linter): reduce noSecrets false positives on CamelCase identifiers (#8832)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 652cfbb commit b08270b

6 files changed

Lines changed: 142 additions & 16 deletions

File tree

.changeset/brave-camels-dance.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#8809](https://github.com/biomejs/biome/issues/8809), [#7985](https://github.com/biomejs/biome/issues/7985), and [#8136](https://github.com/biomejs/biome/issues/8136): the `noSecrets` rule no longer reports false positives on common CamelCase identifiers like `paddingBottom`, `backgroundColor`, `unhandledRejection`, `uncaughtException`, and `IngestGatewayLogGroup`.
6+
7+
The entropy calculation algorithm now uses "average run length" to distinguish between legitimate CamelCase patterns (which have longer runs of same-case letters) and suspicious alternating case patterns (which have short runs).

crates/biome_js_analyze/src/lint/security/no_secrets.rs

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -418,11 +418,18 @@ Uses Shannon Entropy as a base algorithm, then adds "boosts" for special pattern
418418
For example, Continuous mixed cases (lIkE tHiS) are more likely to contribute to a higher score than single cases.
419419
Symbols also contribute highly to secrets.
420420
421-
TODO: This needs work. False positives/negatives are highlighted in valid.js and invalid.js.
421+
Key insight: CamelCase/PascalCase has a predictable pattern where uppercase letters start "words"
422+
and are followed by runs of lowercase letters. Random/suspicious strings have more frequent
423+
case switches with shorter runs between them.
424+
425+
We measure "average run length" - the average number of same-case letters before a switch.
426+
CamelCase has longer runs (e.g., "Gateway" = 1 upper + 6 lower = avg 3.5)
427+
Random alternating has short runs (e.g., "aBcDeF" = all runs of length 1)
422428
423429
References:
424430
- ChatGPT chat: https://chatgpt.com/share/670370bf-3e18-8011-8454-f3bd01be0319
425431
- Original paper for Shannon Entropy: https://ieeexplore.ieee.org/abstract/document/6773024/
432+
- Fix for false positives on CamelCase: https://github.com/biomejs/biome/issues/8809
426433
*/
427434
fn calculate_entropy_with_case_and_classes(data: &str) -> f64 {
428435
let shannon_entropy = shannon_entropy(data);
@@ -434,35 +441,56 @@ fn calculate_entropy_with_case_and_classes(data: &str) -> f64 {
434441
let mut digit_count = 0;
435442
let mut symbol_count = 0;
436443
let mut case_switches = 0;
437-
let mut previous_char_was_upper = false;
444+
let mut previous_was_upper: Option<bool> = None;
438445

439-
// Letter classification and case switching
440-
for (i, c) in data.chars().enumerate() {
446+
for c in data.chars() {
441447
if c.is_ascii_alphabetic() {
442448
letter_count += 1;
443-
if c.is_uppercase() {
449+
let is_upper = c.is_uppercase();
450+
451+
if is_upper {
444452
uppercase_count += 1;
445-
if i > 0 && !previous_char_was_upper {
446-
case_switches += 1;
447-
}
448-
previous_char_was_upper = true;
449453
} else {
450454
lowercase_count += 1;
451-
if i > 0 && previous_char_was_upper {
452-
case_switches += 1;
453-
}
454-
previous_char_was_upper = false;
455455
}
456+
457+
// Count case switches (transitions between upper and lower)
458+
if let Some(prev_upper) = previous_was_upper
459+
&& prev_upper != is_upper
460+
{
461+
case_switches += 1;
462+
}
463+
previous_was_upper = Some(is_upper);
456464
} else if c.is_ascii_digit() {
457465
digit_count += 1;
458466
} else if !c.is_whitespace() {
459467
symbol_count += 1;
460468
}
461469
}
462470

463-
// Adjust entropy: case switches and symbol boosts
464-
let case_entropy_boost = if uppercase_count > 0 && lowercase_count > 0 {
465-
(case_switches as f64 / letter_count as f64) * 2.0
471+
// Calculate case entropy boost based on case switch frequency
472+
// The key metric is "average run length" = letters / (switches + 1)
473+
// CamelCase has longer runs (lower boost), random alternating has short runs (higher boost)
474+
let case_entropy_boost = if uppercase_count > 0 && lowercase_count > 0 && letter_count > 0 {
475+
// average_run_length: higher = more CamelCase-like, lower = more random
476+
// For "IngestGatewayLogGroup" (21 letters, 7 switches): avg_run = 21/8 = 2.625
477+
// For "aBcDeFgHiJk" (11 letters, 10 switches): avg_run = 11/11 = 1.0
478+
let average_run_length = letter_count as f64 / (case_switches + 1) as f64;
479+
480+
// Normalize: if avg_run >= 2.5, it's likely CamelCase (no boost)
481+
// if avg_run = 1, it's perfectly alternating (full boost)
482+
// Linear scale between 1 and 2.5, clamped
483+
let camel_factor = if average_run_length >= 2.5 {
484+
0.0 // CamelCase-like, no case boost
485+
} else if average_run_length <= 1.0 {
486+
1.0 // Perfectly alternating, full boost
487+
} else {
488+
// Linear interpolation: (2.5 - avg) / 1.5
489+
(2.5 - average_run_length) / 1.5
490+
};
491+
492+
let switch_density = case_switches as f64 / letter_count as f64;
493+
switch_density * 2.0 * camel_factor
466494
} else {
467495
0.0
468496
};
@@ -557,4 +585,53 @@ mod tests {
557585
"Expected some entropy for digits, got {entropy}"
558586
);
559587
}
588+
589+
#[test]
590+
fn test_camelcase_entropy() {
591+
// CamelCase/PascalCase identifiers should have low entropy (below 4.1 threshold)
592+
// These are common programming identifiers that should NOT trigger false positives
593+
let entropy = calculate_entropy_with_case_and_classes("paddingBottom");
594+
assert!(entropy < 4.1, "paddingBottom should not trigger: {entropy}");
595+
596+
let entropy = calculate_entropy_with_case_and_classes("IngestGatewayLogGroup");
597+
assert!(
598+
entropy < 4.1,
599+
"IngestGatewayLogGroup should not trigger: {entropy}"
600+
);
601+
602+
let entropy = calculate_entropy_with_case_and_classes("unhandledRejection");
603+
assert!(
604+
entropy < 4.1,
605+
"unhandledRejection should not trigger: {entropy}"
606+
);
607+
608+
let entropy = calculate_entropy_with_case_and_classes("backgroundColor");
609+
assert!(
610+
entropy < 4.1,
611+
"backgroundColor should not trigger: {entropy}"
612+
);
613+
614+
let entropy = calculate_entropy_with_case_and_classes("uncaughtException");
615+
assert!(
616+
entropy < 4.1,
617+
"uncaughtException should not trigger: {entropy}"
618+
);
619+
}
620+
621+
#[test]
622+
fn test_alternating_case_entropy() {
623+
// Alternating case patterns should have high entropy (suspicious)
624+
let entropy = calculate_entropy_with_case_and_classes("aBcDeFgHiJkLmNoPq");
625+
assert!(entropy > 4.1, "Alternating case should trigger: {entropy}");
626+
}
627+
628+
#[test]
629+
fn test_secret_detection_unchanged() {
630+
// Ensure real secrets with mixed case, digits, and symbols still trigger
631+
let entropy = calculate_entropy_with_case_and_classes("AKIaSyD9mP+e2KqZ2S");
632+
assert!(
633+
entropy > 6.5,
634+
"AWS-like key should still trigger: {entropy}"
635+
);
636+
}
560637
}

crates/biome_js_analyze/tests/specs/security/noSecrets/invalid.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const VAULT = {
1818
token: "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"
1919
};
2020

21+
// Alternating case patterns - SHOULD trigger (suspicious, unlike CamelCase)
22+
const suspiciousString = "aBcDeFgHiJkLmNoPq";
23+
2124
// TODO: Get these to work, they seem common and important
2225
// const herokuApiKey = "abcd1234-5678-90ef-ghij-klmnopqrstuv";
2326
// const BASIC_AUTH_HEADER = "Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l";

crates/biome_js_analyze/tests/specs/security/noSecrets/invalid.js.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const VAULT = {
2424
token: "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"
2525
};
2626
27+
// Alternating case patterns - SHOULD trigger (suspicious, unlike CamelCase)
28+
const suspiciousString = "aBcDeFgHiJkLmNoPq";
29+
2730
// TODO: Get these to work, they seem common and important
2831
// const herokuApiKey = "abcd1234-5678-90ef-ghij-klmnopqrstuv";
2932
// const BASIC_AUTH_HEADER = "Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l";
@@ -419,3 +422,25 @@ invalid.js:18:10 lint/security/noSecrets ━━━━━━━━━━━━━
419422
420423
421424
```
425+
426+
```
427+
invalid.js:22:26 lint/security/noSecrets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
428+
429+
i Potential secret found.
430+
431+
21// Alternating case patterns - SHOULD trigger (suspicious, unlike CamelCase)
432+
> 22const suspiciousString = "aBcDeFgHiJkLmNoPq";
433+
^^^^^^^^^^^^^^^^^^^
434+
23
435+
24// TODO: Get these to work, they seem common and important
436+
437+
i Type of secret detected: Detected high entropy string
438+
439+
i Storing secrets in source code is a security risk. Consider the following steps:
440+
1. Remove the secret from your code. If you've already committed it, consider removing the commit entirely from your git tree.
441+
2. If needed, use environment variables or a secure secret management system to store sensitive data.
442+
3. If this is a false positive, consider adding an inline disable comment, or tweak the entropy threshold. See options in our docs.
443+
This rule only catches basic vulnerabilities. For more robust, proper solutions, check out our recommendations at: https://biomejs.dev/linter/rules/no-secrets/#recommendations
444+
445+
446+
```

crates/biome_js_analyze/tests/specs/security/noSecrets/valid.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ const tailwindConfigOptions = {
3535
}
3636
export const url = 'https://www.nytimes.com/2024/03/05/arts/design/pritzker-prize-riken-yamamoto-architecture.html'
3737

38+
// CamelCase/PascalCase identifiers - should NOT trigger (fix for #8809, #7985, #8136)
39+
const paddingBottomValue = "paddingBottom";
40+
const bgColorProperty = "backgroundColor";
41+
const rejectionHandler = "unhandledRejection";
42+
const exceptionType = "uncaughtException";
43+
const logGroupName = "IngestGatewayLogGroup";
44+
3845
// TODO: Remove these false positives, they unfortunately hurt the user experience.
3946
// const NAMESPACE_CLASSNAME = 'Validation.JSONSchemaValidationUtilsImplFactory';
4047
// const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

crates/biome_js_analyze/tests/specs/security/noSecrets/valid.js.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ const tailwindConfigOptions = {
4141
}
4242
export const url = 'https://www.nytimes.com/2024/03/05/arts/design/pritzker-prize-riken-yamamoto-architecture.html'
4343

44+
// CamelCase/PascalCase identifiers - should NOT trigger (fix for #8809, #7985, #8136)
45+
const paddingBottomValue = "paddingBottom";
46+
const bgColorProperty = "backgroundColor";
47+
const rejectionHandler = "unhandledRejection";
48+
const exceptionType = "uncaughtException";
49+
const logGroupName = "IngestGatewayLogGroup";
50+
4451
// TODO: Remove these false positives, they unfortunately hurt the user experience.
4552
// const NAMESPACE_CLASSNAME = 'Validation.JSONSchemaValidationUtilsImplFactory';
4653
// const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

0 commit comments

Comments
 (0)