CWE-798: Use of Hard-coded Credentials - JavaScript/Node.js
Overview
Hard-coded credentials in JavaScript/Node.js applications occur when sensitive values (passwords, API keys, database credentials, JWT secrets, encryption keys) are embedded directly in source code or configuration files. This is especially dangerous in Node.js where code is often deployed to cloud platforms, containers, or serverless environments, and .env files are frequently committed to version control by mistake.
Primary Defence: Use environment variables with process.env, dotenv package for local development (with .env in .gitignore), or cloud secrets managers (AWS Secrets Manager, Azure Key Vault).
Common Vulnerable Patterns
Hard-coded Database Credentials
const mongoose = require('mongoose');
// VULNERABLE - Database password in code
const mongoUri = 'mongodb://admin:P@ssw0rd123@localhost:27017/myapp';
mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Password visible to all developers, in version control, and backups
Why this is vulnerable: Credential exposed in source code. Anyone with repository access can extract password.
Hard-coded API Keys in Express
const express = require('express');
const stripe = require('stripe')('sk_live_4eC39HqLyjWDarjtT1zdp7dc'); // VULNERABLE
const app = express();
app.post('/charge', async (req, res) => {
try {
const charge = await stripe.charges.create({
amount: req.body.amount,
currency: 'usd',
source: req.body.token
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Why this is vulnerable: Stripe secret key hard-coded. Anyone with code access can make charges.
JWT Secret in Code
const jwt = require('jsonwebtoken');
// VULNERABLE - JWT secret hard-coded
const JWT_SECRET = 'my-super-secret-key-12345';
function generateToken(userId) {
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '24h' });
}
function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
return null;
}
}
Why this is vulnerable: Anyone with secret can forge JWTs, impersonate users, bypass authentication.
AWS Credentials in Config File
// config/aws.js - VULNERABLE
const AWS = require('aws-sdk');
AWS.config.update({
accessKeyId: 'AKIAIOSFODNN7EXAMPLE', // VULNERABLE
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', // VULNERABLE
region: 'us-east-1'
});
const s3 = new AWS.S3();
module.exports = { s3 };
Why this is vulnerable: AWS credentials in code. Anyone can access S3 buckets, potentially entire AWS account.
.env File Committed to Git
// .env file (VULNERABLE if committed to git)
DATABASE_URL=postgres://admin:MyP@ssw0rd@db.example.com:5432/production
STRIPE_SECRET_KEY=sk_live_51H...
JWT_SECRET=super-secret-jwt-key
SENDGRID_API_KEY=SG.abc123...
// app.js
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL; // Reads from .env
// If .env committed to git, all secrets exposed in repository history
Why this is vulnerable: .env file in version control persists forever in git history, even after deletion.
Hard-coded Encryption Keys
const crypto = require('crypto');
// VULNERABLE - Encryption key hard-coded
const ENCRYPTION_KEY = 'abcdef1234567890abcdef1234567890'; // 32 bytes
const IV_LENGTH = 16;
function encrypt(text) {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
Why this is vulnerable: All encrypted data can be decrypted if attacker gets source code.
SMTP Credentials in Code
const nodemailer = require('nodemailer');
// VULNERABLE - Email credentials hard-coded
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: 'myapp@gmail.com',
pass: 'MyEmailPassword123' // VULNERABLE
}
});
async function sendEmail(to, subject, text) {
await transporter.sendMail({
from: 'myapp@gmail.com',
to,
subject,
text
});
}
Why this is vulnerable: SMTP password exposed. Attacker can send spam, phishing emails from account.
OAuth Client Secret in Frontend
// React/Vue frontend code - VULNERABLE
const OAUTH_CONFIG = {
clientId: 'abc123xyz',
clientSecret: 'secret_abc123xyz789', // VULNERABLE IN CLIENT-SIDE CODE
redirectUri: 'https://myapp.com/callback'
};
function initiateOAuth() {
// OAuth flow with exposed client secret
window.location.href = `https://oauth.provider.com/authorize?client_id=${OAUTH_CONFIG.clientId}&client_secret=${OAUTH_CONFIG.clientSecret}`;
}
Why this is vulnerable: Client secret visible in browser source code. Anyone can impersonate application.
Secure Patterns
SECURE: Environment Variables with dotenv
// .env file (NOT committed to git, in .gitignore)
DATABASE_URL=mongodb://admin:SecurePass@localhost:27017/myapp
STRIPE_SECRET_KEY=sk_live_actual_key_here
JWT_SECRET=randomly_generated_secret_key
// app.js
require('dotenv').config();
const mongoose = require('mongoose');
// SECURE - Credentials from environment
const mongoUri = process.env.DATABASE_URL;
if (!mongoUri) {
throw new Error('DATABASE_URL environment variable not set');
}
mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
Why this works: The .env file stores credentials outside source code and is excluded from version control via .gitignore. Environment variables are loaded at runtime using dotenv, keeping secrets separate from the codebase. The validation ensures fail-fast behavior if credentials are missing. Credentials can be rotated by updating .env without code changes. Different .env files can be used per environment (dev, staging, prod) without modifying code.
SECURE: AWS SDK with IAM Roles (No Hard-coded Keys)
// SECURE - No credentials in code
const AWS = require('aws-sdk');
// AWS SDK automatically uses IAM role credentials when running on:
// - EC2 instances with IAM instance profile
// - ECS tasks with IAM task role
// - Lambda functions with execution role
// - Local development: AWS CLI credentials (~/.aws/credentials)
const s3 = new AWS.S3({
region: process.env.AWS_REGION || 'us-east-1'
});
// No accessKeyId or secretAccessKey needed!
async function uploadFile(bucket, key, body) {
await s3.putObject({
Bucket: bucket,
Key: key,
Body: body
}).promise();
}
module.exports = { uploadFile };
Why this works: AWS SDK automatically uses IAM roles from the execution environment (EC2 instance profile, ECS task role, Lambda execution role). No access keys in code - credentials are temporary tokens provided by AWS STS, automatically rotated every few hours. IAM policies control fine-grained access. Locally, the SDK uses AWS CLI credentials (~/.aws/credentials). This eliminates the most common credential leak vector: hardcoded AWS keys in source code.
SECURE: AWS Secrets Manager Integration
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager({
region: process.env.AWS_REGION || 'us-east-1'
});
async function getSecret(secretName) {
try {
const data = await secretsManager.getSecretValue({
SecretId: secretName
}).promise();
if ('SecretString' in data) {
return JSON.parse(data.SecretString);
}
// Binary secret
const buff = Buffer.from(data.SecretBinary, 'base64');
return buff.toString('ascii');
} catch (error) {
console.error('Error retrieving secret:', error);
throw error;
}
}
// Usage
async function initDatabase() {
const dbCredentials = await getSecret('prod/database/credentials');
const mongoose = require('mongoose');
await mongoose.connect(
`mongodb://${dbCredentials.username}:${dbCredentials.password}@${dbCredentials.host}/${dbCredentials.database}`
);
}
initDatabase();
Why this works: AWS Secrets Manager provides centralized secret storage with encryption at rest (KMS), automatic rotation policies, and fine-grained IAM access control. Secrets are retrieved at runtime via SDK (which uses IAM roles, not hardcoded credentials). The JSON structure allows storing multiple related credentials together. Secrets Manager integrates with CloudTrail for audit logging and supports secret versioning for gradual rotation. The application never stores credentials - it fetches them on-demand.
SECURE: Azure Key Vault Integration
const { SecretClient } = require('@azure/keyvault-secrets');
const { DefaultAzureCredential } = require('@azure/identity');
// SECURE - Uses managed identity or Azure CLI credentials
const vaultUrl = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;
const credential = new DefaultAzureCredential();
const client = new SecretClient(vaultUrl, credential);
async function getSecret(secretName) {
try {
const secret = await client.getSecret(secretName);
return secret.value;
} catch (error) {
console.error('Error retrieving secret:', error);
throw error;
}
}
// Usage
async function initStripe() {
const stripeKey = await getSecret('stripe-secret-key');
const stripe = require('stripe')(stripeKey);
return stripe;
}
module.exports = { initStripe };
Why this works: Azure Key Vault centralizes secret management with enterprise-grade security (FIPS 140-2 Level 2 validated HSMs). DefaultAzureCredential uses Managed Identity in Azure environments (App Service, Functions, VMs, AKS) or Azure CLI credentials locally - no credentials in code. Secrets are encrypted at rest and in transit, with full Azure Monitor audit logs. Supports secret versioning, soft-delete protection, and RBAC for fine-grained access control. Credentials retrieved on-demand, not stored in application.
SECURE: Google Cloud Secret Manager
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
// SECURE - Uses application default credentials (ADC)
// Automatically works in GCP (service account) and local dev (gcloud auth)
const client = new SecretManagerServiceClient();
async function getSecret(projectId, secretName, version = 'latest') {
const name = `projects/${projectId}/secrets/${secretName}/versions/${version}`;
try {
const [response] = await client.accessSecretVersion({ name });
const payload = response.payload.data.toString('utf8');
return payload;
} catch (error) {
console.error('Error accessing secret:', error);
throw error;
}
}
// Usage
async function initApp() {
const projectId = process.env.GCP_PROJECT_ID;
const jwtSecret = await getSecret(projectId, 'jwt-secret');
const dbPassword = await getSecret(projectId, 'database-password');
// Use credentials...
}
initApp();
Why this works: Google Cloud Secret Manager stores credentials encrypted with customer-managed or Google-managed keys and exposes them only over authenticated API calls. Application Default Credentials pull short-lived tokens from the runtime (GCE, GKE, Cloud Run, or local gcloud auth), so you never hardcode keys in source. Access is gated by IAM roles, and every secret access is logged to Cloud Audit Logs for traceability. Because secrets are fetched at runtime, rotation is centralized - update the version in Secret Manager and deploy with the same code. Versioning lets you stage new values and roll back if needed. The code fetches only the specific secret versions it needs, minimizing blast radius.
SECURE: HashiCorp Vault Integration
const vault = require('node-vault');
// SECURE - Vault client with token from environment
const vaultClient = vault({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN // From CI/CD or local dev
});
async function getSecret(path) {
try {
const result = await vaultClient.read(path);
return result.data;
} catch (error) {
console.error('Error reading from Vault:', error);
throw error;
}
}
// Usage
async function getDatabaseCredentials() {
const secrets = await getSecret('secret/data/database');
return {
host: secrets.host,
username: secrets.username,
password: secrets.password
};
}
module.exports = { getDatabaseCredentials };
Why this works: HashiCorp Vault keeps secrets off disk and out of source control, enforcing access through tokens delivered by your platform (CI/CD, Kubernetes auth, AppRole) instead of hardcoded keys. The Vault client talks over TLS, and Vault audit logs every request so you can trace who accessed what. Centralizing secrets means rotation is a Vault operation - no code change required - and leases can expire automatically for dynamic secrets. Keeping the token in the environment (or injected at runtime) prevents it from entering git history, and the helper function reads only the intended path, reducing accidental exposure of other paths.
SECURE: Kubernetes Secrets
// SECURE - Read secrets mounted as files in Kubernetes
const fs = require('fs').promises;
const path = require('path');
async function getSecret(secretName) {
// Kubernetes mounts secrets to /var/run/secrets/
const secretPath = path.join('/var/run/secrets', secretName);
try {
const secret = await fs.readFile(secretPath, 'utf8');
return secret.trim();
} catch (error) {
console.error(`Error reading secret ${secretName}:`, error);
throw error;
}
}
// Usage
async function initDatabase() {
const dbPassword = await getSecret('database-password');
const dbHost = process.env.DATABASE_HOST;
const mongoose = require('mongoose');
await mongoose.connect(
`mongodb://admin:${dbPassword}@${dbHost}/myapp`
);
}
initDatabase();
Why this works: Kubernetes Secrets are mounted into the pod filesystem at runtime, so images and source never contain credentials. Access to the Secret object is governed by namespace scoping and RBAC, reducing who can read it. Because the secret value is read from the mounted file each time, updating the Secret in the cluster rotates the value without rebuilding the container; with volume projection and reload logic you can rotate without a full pod restart. Files on disk are in-memory tmpfs on many distros, limiting persistence. This pattern keeps secrets local to the cluster control plane rather than environment variables baked into deployments.
SECURE: JWT Secret from Environment with Validation
const jwt = require('jsonwebtoken');
// SECURE - JWT secret from environment, validated
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRY = process.env.JWT_EXPIRY || '24h';
// Validate on startup
if (!JWT_SECRET || JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be set and at least 32 characters');
}
function generateToken(userId) {
return jwt.sign(
{ userId },
JWT_SECRET,
{
expiresIn: JWT_EXPIRY,
issuer: 'myapp',
audience: 'myapp-users'
}
);
}
function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET, {
issuer: 'myapp',
audience: 'myapp-users'
});
} catch (error) {
return null;
}
}
module.exports = { generateToken, verifyToken };
Why this works: Loading the JWT secret from the environment keeps it out of source control and container images, and the startup guard enforces minimum length so weak keys fail fast instead of deploying insecure defaults. Validation happens before the app serves requests, preventing tokens signed with short or missing secrets. By centralizing the secret in env/secret stores, rotation is straightforward: set a new value and restart, pairing with dual-secret rotation patterns if needed. The code also keeps the secret out of logs and restricts issuer/audience claims, reducing misuse. This keeps signing keys ephemeral and externally managed rather than hardcoded.
Key Security Functions
Startup Validation for Required Secrets
function validateRequiredEnvVars() {
const required = [
'DATABASE_URL',
'JWT_SECRET',
'STRIPE_SECRET_KEY',
'AWS_REGION'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('Missing required environment variables:', missing);
process.exit(1);
}
// Validate secret strength
if (process.env.JWT_SECRET.length < 32) {
console.error('JWT_SECRET must be at least 32 characters');
process.exit(1);
}
}
// Call at application startup
validateRequiredEnvVars();
Secrets Caching with TTL
class SecretsCache {
constructor(ttl = 3600000) { // 1 hour default
this.cache = new Map();
this.ttl = ttl;
}
async getSecret(key, fetchFn) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.value;
}
// Fetch fresh secret
const value = await fetchFn(key);
this.cache.set(key, {
value,
timestamp: Date.now()
});
return value;
}
clear() {
this.cache.clear();
}
}
// Usage
const secretsCache = new SecretsCache();
async function getDbPassword() {
return secretsCache.getSecret('db-password', async (key) => {
// Fetch from AWS Secrets Manager, Azure Key Vault, etc.
return await fetchFromSecretsManager(key);
});
}
Redact Secrets from Logs
function redactSecrets(obj) {
const secretKeys = ['password', 'secret', 'token', 'key', 'apiKey', 'apiSecret'];
const redacted = { ...obj };
Object.keys(redacted).forEach(key => {
const lowerKey = key.toLowerCase();
if (secretKeys.some(secretKey => lowerKey.includes(secretKey))) {
redacted[key] = '[REDACTED]';
}
});
return redacted;
}
// Usage in logging
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.combine(
winston.format((info) => {
if (info.metadata) {
info.metadata = redactSecrets(info.metadata);
}
return info;
})(),
winston.format.json()
),
transports: [new winston.transports.File({ filename: 'app.log' })]
});
// Secrets automatically redacted
logger.info('User login', {
metadata: {
username: 'user@example.com',
password: 'secret123' // Will be [REDACTED]
}
});
Rotate Secrets Gracefully
class SecretRotation {
constructor(primaryKey, secondaryKey) {
this.primary = primaryKey;
this.secondary = secondaryKey;
}
// Try primary, fallback to secondary during rotation
verify(token) {
try {
return jwt.verify(token, this.primary);
} catch (primaryError) {
try {
return jwt.verify(token, this.secondary);
} catch (secondaryError) {
return null;
}
}
}
sign(payload) {
// Always sign with primary
return jwt.sign(payload, this.primary);
}
}
// Usage
const secrets = new SecretRotation(
process.env.JWT_SECRET_PRIMARY,
process.env.JWT_SECRET_SECONDARY // Old key during rotation
);
module.exports = { secrets };
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced
Analysis Steps
Locate the Hard-coded Credential
// Line 15 in src/config/database.js
const mongoUri = 'mongodb://admin:MyP@ssw0rd123@localhost:27017/myapp';
Identify Credential Type
- Database password (MongoDB)
- Other types: API keys, JWT secrets, encryption keys, AWS credentials, OAuth secrets
Assess Exposure
- Is file in version control? (Check
git log src/config/database.js) - Is credential valid for production? (Critical if yes)
- How many developers have access?
- Is repository public or has been public?
Determine Scope
- Search for other hard-coded credentials:
grep -r "password\s*=\s*['\"]" . - Check git history:
git log -p -S "MyP@ssw0rd123" - Scan for patterns: Use TruffleHog or Gitleaks
Verification
After remediation:
- Credential removed from code (check with
grep) -
.envfile in.gitignore(verify withgit status) - Application works with environment variable (test locally)
- Credential rotated if exposed in version control
- Scanner re-scan shows finding resolved
- No similar issues remain (full codebase scan)
Storing Secrets in package.json
{
"name": "myapp",
"version": "1.0.0",
"scripts": {
"start": "DATABASE_PASSWORD=MyP@ss node app.js"
}
}
Why wrong: package.json committed to git. Secrets in version control.
Use Environment Variables
Security Checklist
- No credentials hard-coded in source code
-
.envfile in.gitignore(never committed) - Use environment variables for all secrets
- Implement startup validation for required secrets
- Use cloud secrets manager (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager)
- For AWS: Use IAM roles instead of access keys
- For Azure: Use managed identities
- For GCP: Use service accounts with ADC
- No secrets in Docker images (use runtime injection)
- No secrets in client-side code (use backend proxy)
- Implement pre-commit hooks to detect secrets
- Use GitHub secret scanning or Gitleaks
- Rotate secrets regularly (maintain old key during transition)
- Redact secrets from logs
- Use
.env.exampleas template (without actual values) - Audit git history for accidentally committed secrets
- Never log
process.envor full configuration objects