Hardcoded API Keys in JavaScript/Node.js Applications

Critical Risk Secrets Exposure
api-keysjavascriptnodejssecretshardcoded-credentialssecurity-keysauthentication

What it is

A critical security vulnerability where sensitive API keys, tokens, and credentials are hardcoded directly in JavaScript source code. This exposes secrets to anyone with access to the codebase, including version control history, making systems vulnerable to unauthorized access and data breaches.

// VULNERABLE: Multiple hardcoded secrets in a payment service const express = require('express'); const stripe = require('stripe')('stripe_live_EXAMPLE123456789abcdef'); // Hardcoded! const twilio = require('twilio')('AC1234567890abcdef', 'auth_token_123456'); // Hardcoded! class PaymentService { constructor() { // More hardcoded secrets this.webhookSecret = 'whsec_1234567890abcdefghijklmnop'; this.encryptionKey = 'encryption_key_super_secret_123'; } async processPayment(amount, customer) { try { const paymentIntent = await stripe.paymentIntents.create({ amount: amount, currency: 'usd', customer: customer.id }); // Send SMS notification using hardcoded Twilio credentials await twilio.messages.create({ body: `Payment of $${amount/100} processed successfully`, from: '+1234567890', to: customer.phone }); return paymentIntent; } catch (error) { console.error('Payment processing failed:', error); throw error; } } } module.exports = PaymentService;
// SECURE: Using environment variables and proper secret management const express = require('express'); // Validate required environment variables at startup const requiredEnvVars = [ 'STRIPE_SECRET_KEY', 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'STRIPE_WEBHOOK_SECRET', 'ENCRYPTION_KEY' ]; requiredEnvVars.forEach(envVar => { if (!process.env[envVar]) { console.error(`Missing required environment variable: ${envVar}`); process.exit(1); } }); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const twilio = require('twilio')( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN ); class PaymentService { constructor() { this.webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; this.encryptionKey = process.env.ENCRYPTION_KEY; } async processPayment(amount, customer) { try { const paymentIntent = await stripe.paymentIntents.create({ amount: amount, currency: 'usd', customer: customer.id }); // Send SMS notification using environment-based credentials await twilio.messages.create({ body: `Payment of $${amount/100} processed successfully`, from: process.env.TWILIO_PHONE_NUMBER, to: customer.phone }); return paymentIntent; } catch (error) { console.error('Payment processing failed:', error); throw error; } } // Method to verify webhook signatures verifyWebhook(payload, signature) { try { return stripe.webhooks.constructEvent( payload, signature, this.webhookSecret ); } catch (error) { console.error('Webhook verification failed:', error); throw error; } } } module.exports = PaymentService;

💡 Why This Fix Works

The vulnerable code hardcodes multiple sensitive API keys and secrets directly in the source code, making them visible to anyone with code access. The secure version uses environment variables, validates their presence at startup, and properly manages secret access.

// config/production.js - VULNERABLE production configuration module.exports = { database: { host: 'prod-db-cluster.us-east-1.rds.amazonaws.com', port: 5432, name: 'company_prod', username: 'prod_admin', password: 'ProdPassword123!@#' // NEVER hardcode passwords! }, redis: { host: 'prod-redis.abc123.cache.amazonaws.com', port: 6379, password: 'RedisSecretPassword456$%^' // Hardcoded Redis password! }, external: { aws: { accessKeyId: 'AKIAIOSFODNN7EXAMPLE', // Hardcoded AWS credentials! secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', region: 'us-east-1' }, sendgrid: { apiKey: 'SG.1234567890abcdefghijklmnopqrstuvwxyz' // Hardcoded SendGrid API key! }, slack: { webhookUrl: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX' } }, security: { jwtSecret: 'super_secret_jwt_key_that_should_not_be_here', // JWT secret exposed! encryptionKey: 'another_hardcoded_encryption_key_32bytes' } };
// config/production.js - SECURE production configuration const requiredConfig = [ 'DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USERNAME', 'DB_PASSWORD', 'REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'SENDGRID_API_KEY', 'SLACK_WEBHOOK_URL', 'JWT_SECRET', 'ENCRYPTION_KEY' ]; // Validate all required environment variables are present const missingConfig = requiredConfig.filter(key => !process.env[key]); if (missingConfig.length > 0) { console.error('Missing required environment variables:', missingConfig); process.exit(1); } module.exports = { database: { host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT), name: process.env.DB_NAME, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD }, redis: { host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT), password: process.env.REDIS_PASSWORD }, external: { aws: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, region: process.env.AWS_REGION }, sendgrid: { apiKey: process.env.SENDGRID_API_KEY }, slack: { webhookUrl: process.env.SLACK_WEBHOOK_URL } }, security: { jwtSecret: process.env.JWT_SECRET, encryptionKey: process.env.ENCRYPTION_KEY } }; // Optional: Add configuration validation function validateConfiguration(config) { // Validate JWT secret length if (config.security.jwtSecret.length < 32) { throw new Error('JWT secret must be at least 32 characters long'); } // Validate encryption key length if (config.security.encryptionKey.length !== 32) { throw new Error('Encryption key must be exactly 32 characters long'); } return config; } module.exports = validateConfiguration(module.exports);

💡 Why This Fix Works

The vulnerable configuration exposes all sensitive credentials in source code. The secure version uses environment variables, validates their presence, and includes configuration validation to ensure proper setup.

// webpack.config.js - VULNERABLE: Server secrets exposed to client const webpack = require('webpack'); module.exports = { // ... other webpack config plugins: [ new webpack.DefinePlugin({ // DANGEROUS: These server secrets will be included in the client bundle! 'process.env.DATABASE_URL': JSON.stringify('postgresql://user:password@localhost/db'), 'process.env.JWT_SECRET': JSON.stringify('super_secret_jwt_key_123'), 'process.env.STRIPE_SECRET_KEY': JSON.stringify('stripe_live_EXAMPLE123456789abcdef'), 'process.env.SENDGRID_API_KEY': JSON.stringify('SG.1234567890abcdefghijk'), // These are fine for client-side 'process.env.API_URL': JSON.stringify(process.env.API_URL), 'process.env.GOOGLE_MAPS_KEY': JSON.stringify(process.env.GOOGLE_MAPS_KEY) }) ] }; // src/api/client.js - VULNERABLE: Using server secrets in client code class ApiClient { constructor() { // These secrets are now visible in the browser! this.jwtSecret = process.env.JWT_SECRET; this.databaseUrl = process.env.DATABASE_URL; this.stripeKey = process.env.STRIPE_SECRET_KEY; } // Client-side JWT verification (NEVER do this!) verifyToken(token) { return jwt.verify(token, this.jwtSecret); } }
// webpack.config.js - SECURE: Only client-safe variables exposed const webpack = require('webpack'); // Define which environment variables are safe for client-side const clientSafeEnvVars = { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.API_URL': JSON.stringify(process.env.API_URL), 'process.env.GOOGLE_MAPS_API_KEY': JSON.stringify(process.env.GOOGLE_MAPS_API_KEY), 'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN), 'process.env.APP_VERSION': JSON.stringify(process.env.APP_VERSION) }; module.exports = { // ... other webpack config plugins: [ new webpack.DefinePlugin(clientSafeEnvVars) ] }; // src/api/client.js - SECURE: Proper client-side API handling class ApiClient { constructor() { this.baseURL = process.env.API_URL; this.timeout = 10000; // No secrets stored on client-side this.headers = { 'Content-Type': 'application/json' }; } // Secure: Token verification happens on server async authenticateUser(credentials) { const response = await fetch(`${this.baseURL}/auth/login`, { method: 'POST', headers: this.headers, body: JSON.stringify(credentials) }); if (response.ok) { const { token } = await response.json(); // Store token securely (httpOnly cookie is better) localStorage.setItem('authToken', token); return token; } throw new Error('Authentication failed'); } // Include auth token in requests async makeAuthenticatedRequest(endpoint, options = {}) { const token = localStorage.getItem('authToken'); const headers = { ...this.headers, ...(token && { Authorization: `Bearer ${token}` }), ...options.headers }; return fetch(`${this.baseURL}${endpoint}`, { ...options, headers }); } } // Server-side token verification (separate file: server/auth.js) // const jwt = require('jsonwebtoken'); // // function verifyToken(token) { // return jwt.verify(token, process.env.JWT_SECRET); // Secret stays on server // }

💡 Why This Fix Works

The vulnerable configuration exposes server secrets to client-side code where they become visible to anyone. The secure version only exposes client-safe variables and keeps all sensitive operations on the server.

Why it happens

The most common cause is developers directly embedding API keys, database passwords, or other sensitive credentials in JavaScript source files. This often happens during development for convenience but gets accidentally committed to production code. Once in source control, these secrets are permanently exposed in the repository history.

Root causes

Direct Hardcoding in Source Files

The most common cause is developers directly embedding API keys, database passwords, or other sensitive credentials in JavaScript source files. This often happens during development for convenience but gets accidentally committed to production code. Once in source control, these secrets are permanently exposed in the repository history.

Preview example – JAVASCRIPT
// Vulnerable: API key hardcoded in source
const STRIPE_SECRET_KEY = 'stripe_live_EXAMPLE123456789abcdef';
const OPENAI_API_KEY = 'openai_proj_EXAMPLE123456789abcdefghijklmnop';

// Used throughout the application
const stripe = require('stripe')(STRIPE_SECRET_KEY);

Configuration Files in Public Repositories

Developers often store configuration files containing API keys in version control systems, especially when these files are needed for application startup. Config files like config.js, constants.js, or settings.json containing secrets are particularly dangerous when committed to public repositories or shared codebases.

Preview example – JAVASCRIPT
// config/keys.js - VULNERABLE configuration file
module.exports = {
  database: {
    host: 'prod-db.company.com',
    user: 'admin',
    password: 'SuperSecret123!'
  },
  external: {
    twilioAccountSid: 'AC_EXAMPLE_1234567890abcdef',
    twilioAuthToken: 'EXAMPLE_abcdef1234567890ghijklmnop',
    googleMapsApiKey: 'GMAPS_EXAMPLE_Vl0123456789abcdefghijklmnop'
  }
};

Client-Side Secret Exposure

A particularly severe issue occurs when secrets intended for server-side use are accidentally included in client-side JavaScript bundles. Build tools may inadvertently include server secrets in frontend code, making them visible to anyone who inspects the browser's source code or network requests.

Preview example – JAVASCRIPT
// frontend/src/utils/api.js - VULNERABLE: Server secrets in client code
const API_CONFIG = {
  baseURL: 'https://api.company.com',
  // NEVER do this - server secrets in client code!
  adminApiKey: 'admin_key_987654321abcdef',
  databaseSecret: 'db_secret_123456789'
};

// This gets bundled and sent to browsers
export default API_CONFIG;

Fixes

1

Use Environment Variables

Store all sensitive credentials in environment variables rather than hardcoding them in source files. Use process.env to access these variables at runtime. Create .env files for local development and use proper secret management in production environments. Always add .env files to .gitignore to prevent accidental commits.

View implementation – JAVASCRIPT
// Secure approach using environment variables
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

// config/database.js - secure configuration
module.exports = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME
};
2

Implement Secret Management Systems

Use dedicated secret management solutions like AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, or cloud provider secret services. These systems provide encrypted storage, access controls, audit logging, and secret rotation capabilities. Integrate these services into your application to fetch secrets at runtime.

View implementation – JAVASCRIPT
// Using AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

async function getSecret(secretName) {
  try {
    const result = await secretsManager.getSecretValue({
      SecretId: secretName
    }).promise();
    return JSON.parse(result.SecretString);
  } catch (error) {
    console.error('Error retrieving secret:', error);
    throw error;
  }
}

// Usage
const dbCredentials = await getSecret('prod/database/credentials');
const apiKeys = await getSecret('prod/external/api-keys');
3

Separate Client and Server Configurations

Maintain strict separation between client-side and server-side configurations. Use build-time environment variable injection for client-safe values only. Never include server secrets in client builds. Use webpack DefinePlugin or similar tools to inject only non-sensitive configuration into client bundles.

View implementation – JAVASCRIPT
// Client-safe configuration (can be public)
// config/client.js
module.exports = {
  apiUrl: process.env.REACT_APP_API_URL,
  googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY, // Public key
  sentryDsn: process.env.REACT_APP_SENTRY_DSN
};

// Server-only configuration (never exposed to client)
// config/server.js
module.exports = {
  jwtSecret: process.env.JWT_SECRET,
  databaseUrl: process.env.DATABASE_URL,
  stripeSecretKey: process.env.STRIPE_SECRET_KEY
};

Detect This Vulnerability in Your Code

Sourcery automatically identifies hardcoded api keys in javascript/node.js applications and many other security issues in your codebase.