DEV Community

Patience Mpofu
Patience Mpofu

Posted on

Why the Variable Name Is the Most Important Feature in Secrets Detection

ere's a question that sounds trivial until you think about it carefully.

Are these two lines of code equally dangerous?

checksum = "d8e8fca2dc0f896fd7cb4cb0031ba249"
password = "d8e8fca2dc0f896fd7cb4cb0031ba249"
Enter fullscreen mode Exit fullscreen mode

The string value is identical. The entropy is identical. Every character-level feature is identical. A regex scanner treats them the same. A pure entropy scanner treats them the same. A human security engineer does not treat them the same — not even slightly.

The first is almost certainly a file integrity hash. The second is almost certainly an exposed credential. The only difference is the four characters before the equals sign.

When I trained my secrets detector and examined the feature importances, the variable name risk score came out at 0.28 — higher than Shannon entropy, higher than all character distribution features, higher than string length. The single most predictive signal for whether a string is a secret is not the string itself. It's what the developer named the variable holding it.

This article is about what that finding reveals — about how secrets detection actually works, about how developers accidentally expose credentials, and about what it means for how we should think about this entire problem class.


Why Feature Importance of 0.28 Is Remarkable

In a Random Forest model, feature importance is measured by how much each feature reduces impurity across all decision trees. An importance of 0.28 out of 1.0, across 26 features, means the variable name alone accounts for more than a quarter of the model's predictive power.

To put that in context: if you removed every other feature and kept only the variable name, you'd still have a classifier that makes correct decisions on the majority of cases. If you kept every other feature and removed the variable name, you'd lose more predictive power than any other single change.

That's not what I expected when I designed the feature vector. I expected entropy to dominate — it's the signal that most secrets detection literature focuses on. The finding that variable names outperform entropy forced me to rethink some assumptions about the problem.


What Variable Names Actually Encode

Variable names in production code are not arbitrary. They're communication.

When a developer writes api_key = "...", they're not just labelling a memory location. They're documenting their intent. They're telling the next engineer — and, it turns out, a machine learning classifier — that this value is an API key, that it's sensitive, that it should be treated as a secret.

Developers are remarkably consistent about this. Across codebases, languages, and organisations, the same small vocabulary appears around credential storage:

password, passwd, pwd
secret, secret_key, client_secret
api_key, apikey, api_token
token, access_token, auth_token, bearer_token
private_key, privkey, pem
credential, credentials, creds
database_url, db_url, connection_string
Enter fullscreen mode Exit fullscreen mode

And a complementary vocabulary appears around non-sensitive high-entropy strings:

checksum, hash, digest, fingerprint
uuid, guid, id, identifier
version, release, build
color, colour, hex
integrity, signature (in package manifest contexts)
Enter fullscreen mode Exit fullscreen mode

The signal isn't perfect — id sometimes refers to a sensitive identifier, token is sometimes used for pagination tokens or CSRF tokens that aren't secrets in the traditional sense. But the correlation between variable name semantics and actual sensitivity is strong enough to be the most predictive single feature in a 26-dimensional model.


The Three Ways Developers Name Credential Variables

Understanding how variable names signal secrets requires understanding the patterns developers actually use. In practice, there are three distinct naming patterns, each with different detection implications.

Pattern 1: Direct and Obvious

The developer uses a name that directly and unambiguously identifies the value as sensitive:

STRIPE_SECRET_KEY = "sk_live_abc123..."
DATABASE_PASSWORD = "Tr0ub4dor&3"
GITHUB_ACCESS_TOKEN = "ghp_abc123..."
JWT_SECRET = "my-super-secret-signing-key"
Enter fullscreen mode Exit fullscreen mode

These are the easy cases. The variable name scores maximum risk, the classifier is highly confident, and the finding is genuine in nearly every instance. There's no ambiguity to resolve.

Pattern 2: Abbreviated and Conventional

The developer uses a shortened or conventional form that's recognisable within the development community but might be less obvious to an outsider:

DB_PASS = "Winter2019!"          # "PASS" → password
AWS_SK = "wJalrXUtn..."          # "SK" → secret key
OAUTH_CS = "abc123def456..."     # "CS" → client secret
SVC_PWD = "service_password_1"  # "PWD" → password
Enter fullscreen mode Exit fullscreen mode

The risk scoring function handles these through substring matching and a vocabulary of abbreviations. PASS, PWD, SK (in certain contexts), CS, TKN all score high. This is where coverage gaps can appear — an unusual abbreviation in a domain-specific codebase might not be in the vocabulary.

Pattern 3: Contextually Sensitive

The developer uses a name that doesn't obviously indicate sensitivity on its own but becomes sensitive in context:

# In a payment processing module
value = "sk_live_abc123..."       # "value" alone scores 0.1

# In a configuration dictionary
config = {
    "key": "AKIAIOSFODNN7EXAMPLE"  # "key" alone scores 0.7
}

# In a function parameter
def authenticate(token):           # "token" scores 0.9
    headers = {"Authorization": f"Bearer {token}"}
Enter fullscreen mode Exit fullscreen mode

These cases are where the feature vector struggles most. The variable name score for value is 0.1 — weak evidence of sensitivity. In isolation, the classifier would likely pass this. But if the string value itself has high entropy and matches a known pattern (like the Stripe key format), the pattern flags compensate.

This interaction — where a weak key name score is overridden by strong pattern match flags — is exactly how the feature vector is supposed to work. No single feature dominates all cases. The combination handles cases that individual features miss.


The Accidental Exposure Psychology

The variable name finding reveals something important about how secrets end up in code in the first place.

Developers don't accidentally commit secrets because they don't know secrets are sensitive. They commit secrets because the friction of not committing them is high at the moment of writing.

The archetypal scenario:

  1. Developer is building a feature that needs an API key
  2. Developer adds the key directly to the code to get the feature working
  3. Developer intends to "move it to environment variables later"
  4. Developer commits the working code
  5. "Later" never comes, or the commit is already in history The variable name in this scenario is almost always informative — the developer names it API_KEY or STRIPE_KEY or DB_PASSWORD because they know exactly what it is. They're not hiding it from themselves. They're just deferring the cleanup.

This is why the variable name is such a strong signal. The developer's intent is encoded in the name. The developer knew this was a secret when they wrote it. The name reflects that knowledge.

The cases where variable names are not informative are the cases where secrets end up in code through a different mechanism — configuration files that get accidentally committed, environment files that get accidentally tracked, secrets embedded in test data that wasn't meant to be realistic. These are harder to catch and require the entropy and pattern features to carry more weight.


Where Variable Name Scoring Breaks Down

Being precise about the limits of this approach matters.

Obfuscated Names

An attacker who knows about this detection vector could theoretically name credential variables to avoid detection:

# Designed to evade variable name scoring
x = "AKIAIOSFODNN7EXAMPLE7"
data_1 = "sk_live_abc123def456ghi789"
temp = "my-database-password-2024"
Enter fullscreen mode Exit fullscreen mode

The first two would still be caught by pattern match flags — the AWS key format and Stripe key format are distinctive enough that entropy and pattern features alone classify them correctly. The third — a human-chosen password stored in a variable named temp — would be at risk of being missed if the entropy is low.

This is a real gap. In practice, deliberately obfuscated variable names are uncommon in the codebases secrets appear in — developers who are trying to hide secrets from their own team are a different threat model than developers who accidentally expose secrets. But the gap exists.

Generic Framework Patterns

Frameworks and ORMs often use generic variable names that pattern-match to high risk scores without holding sensitive values:

# Django ORM — "password" is a field name, not a credential
class User(models.Model):
    password = models.CharField(max_length=128)  # hashed, not plaintext

# Spring Security — "token" is a parameter name
public ResponseEntity<?> authenticate(@RequestBody TokenRequest token) {
Enter fullscreen mode Exit fullscreen mode

In these cases, the value being assigned is a class reference, a method call, or a parameter object — not a string literal containing a secret. The feature extraction pipeline only runs on string literals, so these cases are largely handled correctly at the scanning stage before feature extraction begins.

Internationalised Codebases

The variable name risk vocabulary is English-only. A codebase with variable names in German (Passwort), French (motDePasse), or Portuguese (senha) will have those variables score the default 0.3 rather than 1.0. This is a genuine coverage gap for multinational organisations. Extending the vocabulary to cover common credential-related terms in other languages would be a meaningful improvement.


The Implication for Secrets Management Culture

The variable name finding has an implication beyond detection accuracy. It tells us something about where to focus prevention efforts.

If developers consistently name their credential variables correctly — if DB_PASSWORD always contains a database password and checksum never does — then the signal for detection is strong. The corollary is that the naming is correct precisely because the developer knows the value is sensitive.

This means secrets in code are not primarily a knowledge problem. Developers who commit secrets usually know they're committing secrets. The problem is friction — the path of least resistance at the moment of writing is to hardcode the value and deal with it later.

The most effective prevention isn't education about what a secret is. It's reducing the friction of not hardcoding secrets in the first place. Pre-commit hooks that block commits before they reach the repository — rather than scanners that find secrets after the fact — address the friction problem directly.

Which is exactly what the pre-commit hook in this tool is designed to do. Catch it at the moment of commit, when the developer already knows the variable is named API_KEY and the value looks like a real key. That's the lowest-friction intervention point — a message at the moment of action rather than a report discovered in a CI pipeline hours later.


Improving the Key Name Feature

The current implementation is a static keyword vocabulary with manually assigned scores. It works well but has obvious limitations — it doesn't learn, it doesn't generalise to novel terms, and it requires manual updates when new credential-adjacent terminology emerges.

A more sophisticated approach would train a small embedding model on variable names from open source code, clustering semantically similar names and learning the association between name semantics and credential presence. Something like word2vec trained on a corpus of variable names from public repositories would generalise to svc_acct_passwd or oauth2_bearer_tkn without requiring explicit vocabulary entries.

That's a meaningful improvement but a substantial increase in complexity. The static vocabulary approach handles the vast majority of real-world cases well enough that the engineering investment in embeddings hasn't been justified yet.

The right trigger for that investment would be systematic measurement of false negatives — cases where the classifier misses real secrets. If those misses cluster around variable naming patterns that aren't in the current vocabulary, the embeddings approach becomes worth building.


What This Tells Us About Security Signal in General

The variable name finding is an instance of a broader principle that I keep encountering in application security work: the most useful signals are often not in the content itself, but in the metadata around the content.

The string "d8e8fca2dc0f896fd7cb4cb0031ba249" carries no information about whether it's sensitive. The variable name password carries almost complete information. The content is identical; the context is everything.

This same principle appears elsewhere in AppSec:

  • HTTP request bodies are harder to classify as malicious than request paths, because paths carry semantic intent
  • Log anomaly detection is more effective on log metadata (frequency, source, timing) than log content
  • Phishing detection is more accurate on sender domain patterns than email body content The implication for building security tools is consistent: don't just look at the data. Look at what surrounds the data. Look at what the developer named it, where it came from, when it appeared, what called it.

Context is signal. Often it's more signal than the content itself.


The full key name risk scoring implementation is in secrets_detector/features.py at github.com/pgmpofu/secrets-detector.

Next up: why I chose Random Forest over deep learning for this problem — the full engineering tradeoffs argument, including interpretability, model size, training speed, and what you give up by not going deep.

Top comments (0)