Skip to main content
interlace
Plugin: secure-codingRules

no-hardcoded-credentials

Detects hardcoded passwords, API keys, tokens, and other sensitive credentials in source code

CWE: CWE-522
OWASP Mobile: M1: Improper Credential Usage

Detects hardcoded passwords, API keys, tokens, and other sensitive credentials in source code. This rule is part of eslint-plugin-secure-coding and provides LLM-optimized error messages that AI assistants can automatically fix.

💼 This rule errors by default in the recommended config.

Quick Summary

AspectDetails
CWE ReferenceCWE-798 (Use of Hard-coded Credentials)
SeverityCritical (security vulnerability)
Auto-Fix✅ Yes (suggests environment variables or secret managers)
CategorySecurity
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForAll applications handling sensitive data, API integrations, database connections

Value & investment case

Why this rule pays for itself. Framework: cicd-impact/philosophy.md.

DimensionValue
CWECWE-798 — Use of Hard-coded Credentials
Feedback-loop tierEditor / pre-commit (sub-second) — cheapest layer per the feedback-loop hierarchy
Defensive-layer leverage~10× cheaper than unit-test · ~1,000× cheaper than production rollback · 10,000+× cheaper than customer disclosure — secrets in code are the textbook long-tail disclosure event (cost-ratio anchors)
Niche relevanceCritical: fintech, healthtech, cybersecurity (mandatory disclosure on breach + regulatory penalty) · High: B2B SaaS, infra/devtools · Medium: B2C, marketplaces · Lower (still important): gaming
Investor-frame impactHardcoded credentials → no rotation possible → on detection, full breach disclosure cycle. IBM Cost of a Data Breach 2024: median credentials-related breach $4.5M; healthcare-specific $9.8M. One catch at lint-time prevents the entire cycle.

Read also: philosophy.md §investor-frame · niche-presets.json · analyzer-evaluation-framework.md

Vulnerability and Risk

Vulnerability: Embedding sensitive credentials (like passwords, API keys, or database connection strings) directly in the source code.

Risk: This leads to credential exposure in version control systems, making them accessible to any developer with repository access or attackers if the code is leaked. It also makes credential rotation difficult and error-prone.

Error Message Format

The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:

🔒 CWE-798 OWASP:A04 CVSS:9.8 | Hardcoded Credentials detected | CRITICAL [SOC2,PCI-DSS,HIPAA,GDPR,ISO27001,NIST-CSF]
   Fix: Review and apply the recommended fix | https://owasp.org/Top10/A04_2021/

Message Components

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-798 OWASP:A04 CVSS:9.8
Issue DescriptionSpecific vulnerabilityHardcoded Credentials detected
Severity & ComplianceImpact assessmentCRITICAL [SOC2,PCI-DSS,HIPAA,GDPR,ISO27001,NIST-CSF]
Fix InstructionActionable remediationFollow the remediation steps below
Technical TruthOfficial referenceOWASP Top 10

Rule Details

Hardcoded credentials are one of the most common security vulnerabilities. This rule detects passwords, API keys, tokens, and other sensitive values that are directly embedded in source code, which can be exposed in version control systems.

Why This Matters

IssueImpactSolution
🔒 SecurityCredentials exposed in git historyUse environment variables
🐛 Data BreachAPI keys can be stolen from codeSecret management services
🔐 Access ControlPasswords visible to all developersAWS Secrets Manager, Vault
📊 ComplianceViolates security best practicesCI/CD secret injection

Detection Patterns

The rule decides on the VALUE's shape, never on the key name alone. A credential-shaped name (password, apiKey, secret) is necessary-but-not- sufficient: it can promote an ambiguous value, but it can never turn a message constant into a finding.

That distinction is the whole rule. Name-driven matching reported errors: { password: 'incorrectPassword' } — an i18n error key — at CVSS 9.8, and on a 1,470-file corpus (webpack, lodash, eslint-plugin-import, two NestJS boilerplates) that single pattern was 5 of 10 findings. The genuinely committed 50-character API secret in the same corpus was found by shape.

Tier 1 — structural, reported on shape alone

  • Prefixed API keys: Stripe (sk_live_…, pk_test_…), GitHub OAuth (ghp_, gho_, ghu_, ghs_, ghr_), AWS (AKIA…)
  • JWTs: eyJ… with three dot-separated base64 parts
  • Database connection strings: protocol://user:pass@host
  • Random blobs: 32+ contiguous alphanumeric characters, mixed case, with digits, Shannon entropy ≥ 3.5 bits/char, and no ascending character run. The charset is strict — punctuation rules a value out, because generated-code strings (installedChunkData[1](error);), comma-separated keyword lists and Postgres constraint names (PK_b36bcfe02fc8de3c57a8b2391c2) all carry punctuation that no API key does. The ascending-run check excludes charset constants such as 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', which are maximally high-entropy and the exact opposite of a secret.

Tier 2 — shape AND a credential-named slot

  • Random blobs of 20–31 characters, e.g. { key: 'fyFGb7ywyM37TqDY8nuhAmGW5' }. Shape alone is not enough at this length: CreateUser1715028537217, a TypeORM migration class name, passes every shape test there is.
  • Long base64 / hex strings (32+), which also appear as hashes and IDs
  • Common weak passwords (password, admin, 123456)
  • Any secret-shaped value in a credential-named slot: at least two character classes (or a 20+ high-entropy single-charset blob), no whitespace, and not a "natural word string". That last test is what rejects incorrectPassword, SessionCacheProvider and experimental_onToolExecutionStart — strings made only of pronounceable, dictionary-shaped tokens joined by camelCase or _, - and . separators, with no digits and no symbols. aaAA@123 has four character classes and is reported; Please enter your password has whitespace and is not.

key / keys are treated as weak names — they label cache keys, map keys and i18n keys far more often than API keys — so they only count as credential context when the value is already a random blob by shape.

Examples

❌ Incorrect

// Hardcoded API key
const apiKey = 'sk_live_FAKE_LIVE_KEY_FOR_TESTING_PURPOSES_ONLY_1234567890';

// Hardcoded password
const password = 'admin123';

// Hardcoded JWT token
const token =
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

// Database connection string with credentials
const dbUrl = 'mysql://user:password@localhost:3306/dbname';

// AWS access key
const awsKey = 'AKIAIOSFODNN7EXAMPLE';

✅ Correct

// Environment variable
const apiKey = process.env.API_KEY;

// Secret manager
const password = await getSecret('database-password');

// Configuration service
const token = configService.get('JWT_TOKEN');

// Environment variable for database
const dbUrl = process.env.DATABASE_URL;

// AWS SDK with IAM roles (no keys needed)
const s3 = new AWS.S3(); // Uses IAM role

✅ Also correct — message constants in credential-named slots

These are the false positives the shape gate exists to prevent. The key is named password; the value is an i18n key, a label, or a sentence.

throw new UnprocessableEntityException({
  errors: { password: 'incorrectPassword' }, // ✅ i18n error key, not a secret
});

const errors = { token: 'notFoundToken', secret: 'missingSecret' }; // ✅
const password = 'Please enter your password';                     // ✅ sentence
export const SessionCacheProvider = 'SessionCacheProvider';        // ✅ DI token
const secret = 'experimental_onToolExecutionStart';                // ✅ identifier

✅ Also correct — self-evident placeholders

A value the developer is visibly expected to replace is not a leaked credential. Skipped by default; set allowPlaceholders: false to report them.

const TEST_CREDENTIALS = {
  apiKey: 'test-api-key',
  token: 'xxxxxxxxxxxx',        // ✅ one character repeated
  password: 'changeme',         // ✅ placeholder word
  secret: '<your-secret-here>', // ✅ bracketed template slot
};

const key = '{{API_SECRET}}';   // ✅ also `${…}` and `[…]`

The allowlist applies only to non-structural findings. A JWT, an sk_live_ key, or a postgres://user:pass@host string keeps its shape whatever words it contains, so those still report.

Configuration

{
  rules: {
    'secure-coding/no-hardcoded-credentials': ['error', {
      ignorePatterns: ['^test-'],           // Ignore test credentials
      allowInTests: true,                   // Skip .test./.spec./__tests__ paths
      minLength: 8,                         // Minimum credential length
      detectApiKeys: true,                  // Detect API keys
      detectPasswords: true,                // Detect passwords
      detectTokens: true,                   // Detect tokens
      detectDatabaseStrings: true,         // Detect database strings
      allowPlaceholders: true              // Skip <your-secret-here>, changeme, xxxxxxxx
    }]
  }
}

Options

OptionTypeDefaultDescription
ignorePatternsstring[][]Regex patterns to ignore
allowInTestsbooleantrueSkip credentials in test files
minLengthnumber8Minimum length for credential detection
detectApiKeysbooleantrueDetect API keys
detectPasswordsbooleantrueDetect passwords
detectTokensbooleantrueDetect tokens
detectDatabaseStringsbooleantrueDetect database connection strings
customPatternsobject[][]Custom credential patterns to detect
strategy"env" | "config" | "vault" | "auto""auto"Strategy for fixing hardcoded credentials (auto = smart detection)
allowPlaceholdersbooleantrueSkip self-evident placeholder values (<your-secret-here>, changeme, xxxxxxxx)

Ignoring Test Credentials

{
  rules: {
    'secure-coding/no-hardcoded-credentials': ['error', {
      ignorePatterns: ['^test-', '^mock-', '^fake-']
    }]
  }
}

Reporting Credentials in Test Files

Test-file credentials are skipped by default. A corpus scan found 17 of 18 findings on a real repository were fixtures in integration/auth.test.js, and a credential in a fixture is not an exploitable finding for this rule — committed real secrets are a secret-scanning concern (gitleaks, trufflehog), which scan history and rotate keys. Set allowInTests: false to report them anyway.

{
  rules: {
    'secure-coding/no-hardcoded-credentials': ['error', {
      allowInTests: false  // Report credentials in .test.ts and .spec.ts too
    }]
  }
}

Rule Logic Flow

Best Practices

1. Use Environment Variables

// ✅ Good
const apiKey = process.env.STRIPE_API_KEY;
if (!apiKey) {
  throw new Error('STRIPE_API_KEY is required');
}

2. Use Secret Management Services

// ✅ Good - AWS Secrets Manager
import { SecretsManager } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManager({ region: 'us-east-1' });
const secret = await client.getSecretValue({ SecretId: 'api-keys' });
const apiKey = JSON.parse(secret.SecretString).stripeKey;

3. Use Configuration Services

// ✅ Good - Config service
import { ConfigService } from '@nestjs/config';

@Injectable()
export class ApiService {
  constructor(private config: ConfigService) {}

  getApiKey() {
    return this.config.get<string>('API_KEY');
  }
}

4. Never Commit Credentials


echo "API_KEY=sk_live_FAKE_KEY_FOR_TESTING" >> .env
echo ".env" >> .gitignore

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Credentials from Config

Why: Config values not traced.

// ❌ NOT DETECTED - From config
const password = config.dbPassword;

Mitigation: Use proper secrets management.

Environment Variables

Why: Env var content not analyzed.

// ❌ NOT DETECTED - Env var
const secret = process.env.API_KEY;

Mitigation: Never hardcode or expose secrets.

Dynamic Credential Access

Why: Dynamic property access not traced.

// ❌ NOT DETECTED - Dynamic
const cred = credentials[type];

Mitigation: Audit all credential access patterns.

Resources

Version History

  • 1.3.0 - Initial release with comprehensive credential detection patterns

If this rule caught a real vulnerability in your codebase, ⭐ star the repo — it keeps the detection logic maintained.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.