Skip to content

CWE-326: Inadequate Encryption Strength - JavaScript/Node.js

Overview

Inadequate Encryption Strength in JavaScript/Node.js applications occurs when developers use weak cryptographic algorithms, insufficient key sizes, or deprecated ciphers that fail to protect sensitive data against modern attacks. The Node.js crypto module provides both secure and insecure options, and proper selection is critical for application security.

Common JavaScript Vulnerability Scenarios:

  • Using DES or RC4 instead of AES-256
  • Implementing password hashing with MD5 or SHA-1
  • Using weak cipher modes like ECB
  • Insufficient key derivation iterations for PBKDF2
  • Using deprecated or custom encryption implementations
  • Weak random number generation for cryptographic keys

JavaScript/Node.js Cryptographic Landscape:

  • crypto (built-in): Node.js native cryptography module
  • bcrypt: Industry-standard password hashing library
  • argon2: Modern password hashing (PHC winner)
  • jose: JSON Web Encryption/Signature library
  • sodium-native: Libsodium bindings for Node.js

Framework-Specific Considerations:

  • Express: Use middleware for encryption/decryption
  • Fastify: Leverage plugins for cryptographic operations
  • Next.js: Secure API routes with proper encryption
  • NestJS: Use dependency injection for crypto services

Primary Defence: Use crypto.createCipheriv() with aes-256-gcm for encryption, bcrypt or argon2 for password hashing, and avoid deprecated algorithms like DES, RC4, or MD5.

Common Vulnerable Patterns

Using DES Encryption

const crypto = require('crypto');

class WeakEncryptionService {
  constructor(key) {
    // DES has only 56-bit effective key strength
    this.key = Buffer.from(key).slice(0, 8); // DES requires 8-byte key
    this.algorithm = 'des-ecb'; // ECB mode is also vulnerable
  }

  encryptSSN(ssn) {
    const cipher = crypto.createCipher(this.algorithm, this.key);
    let encrypted = cipher.update(ssn, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    return encrypted;
  }

  decryptSSN(encryptedSSN) {
    const decipher = crypto.createDecipher(this.algorithm, this.key);
    let decrypted = decipher.update(encryptedSSN, 'base64', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }
}

// Express route using weak encryption
const express = require('express');
const app = express();
app.use(express.json());

const encryptor = new WeakEncryptionService('weak_key');

app.post('/api/users', (req, res) => {
  const { ssn } = req.body;
  const encryptedSSN = encryptor.encryptSSN(ssn);

  // Store encryptedSSN in database
  res.json({ status: 'created', encryptedSSN });
});

Why this is vulnerable:

  • DES provides only 56-bit security (easily broken)
  • ECB mode reveals patterns in encrypted data
  • createCipher is deprecated (uses weak key derivation)
  • No authentication or integrity protection

MD5 for Password Hashing

const crypto = require('crypto');

class WeakPasswordService {
  hashPassword(password) {
    // MD5 is cryptographically broken
    return crypto.createHash('md5')
      .update(password)
      .digest('hex');
  }

  verifyPassword(password, hash) {
    const computedHash = this.hashPassword(password);
    return computedHash === hash; // Timing attack vulnerable
  }
}

// Express authentication
const express = require('express');
const app = express();
app.use(express.json());

const passwordService = new WeakPasswordService();
const users = new Map(); // Simulated database

app.post('/register', (req, res) => {
  const { username, password } = req.body;

  // VULNERABLE - MD5 without salt
  const passwordHash = passwordService.hashPassword(password);

  users.set(username, { username, passwordHash });
  res.json({ status: 'registered' });
});

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.get(username);

  if (!user || !passwordService.verifyPassword(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  res.json({ status: 'logged_in' });
});

Why this is vulnerable:

  • MD5 has collision vulnerabilities
  • No salt means identical passwords produce identical hashes
  • Fast hashing enables brute-force attacks
  • Rainbow tables can reverse common passwords
  • String comparison is vulnerable to timing attacks

Weak Key Derivation for AES

const crypto = require('crypto');

class WeakKeyDerivation {
  constructor(password) {
    // Weak: Direct password use without proper derivation
    this.key = crypto.createHash('sha256')
      .update(password)
      .digest();
  }

  encrypt(plaintext) {
    // AES-256 but with weakly derived key
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv);

    let encrypted = cipher.update(plaintext, 'utf8', 'base64');
    encrypted += cipher.final('base64');

    return iv.toString('base64') + ':' + encrypted;
  }

  decrypt(ciphertext) {
    const [ivBase64, encrypted] = ciphertext.split(':');
    const iv = Buffer.from(ivBase64, 'base64');

    const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv);
    let decrypted = decipher.update(encrypted, 'base64', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}

// Fastify route with weak key derivation
const fastify = require('fastify')({ logger: true });

const encryptor = new WeakKeyDerivation('user_password');

fastify.post('/api/encrypt', async (request, reply) => {
  const { data } = request.body;
  const encrypted = encryptor.encrypt(data);

  return { encrypted };
});

Why this is vulnerable:

  • Single SHA-256 hash is not sufficient for key derivation
  • No salt means same password always produces same key
  • No iteration count to slow down brute-force
  • CBC mode without authentication (vulnerable to tampering)

Low Iteration PBKDF2

const crypto = require('crypto');

class WeakPBKDF2 {
  constructor() {
    this.iterations = 1000; // Too low!
    this.keylen = 16; // Only 128 bits
    this.digest = 'sha1'; // Deprecated
    this.salt = 'fixed_salt'; // Fixed salt!
  }

  deriveKey(password) {
    return crypto.pbkdf2Sync(
      password,
      this.salt,
      this.iterations,
      this.keylen,
      this.digest
    );
  }

  encrypt(password, plaintext) {
    const key = this.deriveKey(password);
    const iv = crypto.randomBytes(16);

    const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
    let encrypted = cipher.update(plaintext, 'utf8', 'base64');
    encrypted += cipher.final('base64');

    return iv.toString('base64') + ':' + encrypted;
  }
}

// Next.js API route with weak PBKDF2
export default async function handler(req, res) {
  const weakPbkdf2 = new WeakPBKDF2();

  if (req.method === 'POST') {
    const { password, data } = req.body;
    const encrypted = weakPbkdf2.encrypt(password, data);

    res.status(200).json({ encrypted });
  }
}

Why this is vulnerable:

  • Only 1000 iterations (OWASP recommends 600,000+ for PBKDF2, updated 2023)
  • Fixed salt defeats the purpose of salting
  • AES-128 instead of AES-256
  • SHA-1 instead of SHA-256
  • CBC mode without authentication

Using createCipher (Deprecated)

const crypto = require('crypto');

// WRONG: createCipher is deprecated and insecure
function weakEncrypt(text, password) {
  const cipher = crypto.createCipher('aes192', password);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}

function weakDecrypt(encrypted, password) {
  const decipher = crypto.createDecipher('aes192', password);
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

// Express route using deprecated API
const express = require('express');
const app = express();

app.post('/api/secure-message', express.json(), (req, res) => {
  const { message, password } = req.body;
  const encrypted = weakEncrypt(message, password);

  res.json({ encrypted });
});

Why this is vulnerable:

  • createCipher is deprecated in Node.js
  • Uses weak MD5-based key derivation
  • AES-192 is weaker than AES-256
  • No proper IV management
  • CBC mode without authentication

Custom Encryption Implementation

// NEVER DO THIS: Custom encryption
class CustomEncryption {
  constructor(key) {
    this.key = key;
  }

  // Simple XOR encryption - completely insecure
  encrypt(plaintext) {
    let encrypted = '';
    for (let i = 0; i < plaintext.length; i++) {
      const charCode = plaintext.charCodeAt(i) ^ this.key.charCodeAt(i % this.key.length);
      encrypted += String.fromCharCode(charCode);
    }
    return Buffer.from(encrypted).toString('base64');
  }

  decrypt(ciphertext) {
    const encrypted = Buffer.from(ciphertext, 'base64').toString();
    let decrypted = '';
    for (let i = 0; i < encrypted.length; i++) {
      const charCode = encrypted.charCodeAt(i) ^ this.key.charCodeAt(i % this.key.length);
      decrypted += String.fromCharCode(charCode);
    }
    return decrypted;
  }
}

// NestJS controller with custom encryption
import { Controller, Post, Body } from '@nestjs/common';

@Controller('api')
export class CryptoController {
  private customEncryption = new CustomEncryption('secretkey');

  @Post('encrypt')
  encrypt(@Body() body: { data: string }) {
    const encrypted = this.customEncryption.encrypt(body.data);
    return { encrypted };
  }
}

Why this is vulnerable:

  • Custom cryptography is almost always broken
  • XOR cipher is trivially breakable
  • No authentication
  • Violates Kerckhoffs's principle

Weak Random Number Generation

// WRONG: Using Math.random for cryptographic purposes
function generateWeakToken() {
  return Math.random().toString(36).substring(2);
}

function generateWeakKey() {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let key = '';
  for (let i = 0; i < 32; i++) {
    key += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return key;
}

// Express session with weak token generation
const express = require('express');
const session = require('express-session');

const app = express();
app.use(session({
  genid: () => generateWeakToken(), // Predictable session IDs!
  secret: generateWeakKey(), // Weak secret!
  resave: false,
  saveUninitialized: true
}));

Why this is vulnerable:

  • Math.random() is not cryptographically secure
  • Predictable output can be exploited
  • Session IDs can be guessed
  • Enables session hijacking attacks

SHA-1 for HMAC

const crypto = require('crypto');

class WeakHMAC {
  constructor(secret) {
    this.secret = secret;
    this.algorithm = 'sha1'; // Deprecated
  }

  sign(data) {
    return crypto
      .createHmac(this.algorithm, this.secret)
      .update(data)
      .digest('hex');
  }

  verify(data, signature) {
    const expected = this.sign(data);
    return expected === signature; // Timing attack vulnerable
  }
}

// Express webhook endpoint
const express = require('express');
const app = express();

const hmac = new WeakHMAC('webhook_secret');

app.post('/api/webhook', express.json(), (req, res) => {
  const signature = req.headers['x-signature'];
  const payload = JSON.stringify(req.body);

  if (!hmac.verify(payload, signature)) {
    return res.status(403).json({ error: 'Invalid signature' });
  }

  // Process webhook
  res.json({ status: 'processed' });
});

Why this is vulnerable:

  • SHA-1 has known collision vulnerabilities
  • String comparison is vulnerable to timing attacks
  • Industry standards require SHA-256 or stronger

Secure Patterns

AES-256-GCM Encryption

const crypto = require('crypto');

class SecureAESGCMEncryption {
  constructor(masterKey) {
    // Master key should be 32 bytes (256 bits) for AES-256
    if (!masterKey || masterKey.length !== 32) {
      throw new Error('Master key must be 32 bytes for AES-256');
    }
    this.masterKey = masterKey;
  }

  static generateKey() {
    // Generate cryptographically secure random key
    return crypto.randomBytes(32);
  }

  encryptSSN(ssn) {
    // Generate random 12-byte IV (recommended for GCM)
    const iv = crypto.randomBytes(12);

    // Create cipher with AES-256-GCM
    const cipher = crypto.createCipheriv('aes-256-gcm', this.masterKey, iv);

    // Add authenticated associated data (AAD)
    const aad = Buffer.from('SSN_ENCRYPTION_V1', 'utf8');
    cipher.setAAD(aad);

    // Encrypt
    let encrypted = cipher.update(ssn, 'utf8');
    encrypted = Buffer.concat([encrypted, cipher.final()]);

    // Get authentication tag
    const authTag = cipher.getAuthTag();

    // Combine IV + auth tag + ciphertext
    const combined = Buffer.concat([iv, authTag, encrypted]);

    return combined.toString('base64');
  }

  decryptSSN(encryptedSSN) {
    const combined = Buffer.from(encryptedSSN, 'base64');

    // Extract components
    const iv = combined.slice(0, 12);
    const authTag = combined.slice(12, 28); // 16 bytes
    const encrypted = combined.slice(28);

    // Create decipher
    const decipher = crypto.createDecipheriv('aes-256-gcm', this.masterKey, iv);

    // Set AAD and auth tag
    const aad = Buffer.from('SSN_ENCRYPTION_V1', 'utf8');
    decipher.setAAD(aad);
    decipher.setAuthTag(authTag);

    // Decrypt and verify
    let decrypted = decipher.update(encrypted);
    decrypted = Buffer.concat([decrypted, decipher.final()]);

    return decrypted.toString('utf8');
  }
}

// Short usage
const key = SecureAESGCMEncryption.generateKey();
const aes = new SecureAESGCMEncryption(key);
const encrypted = aes.encryptSSN('123-45-6789');
const decrypted = aes.decryptSSN(encrypted);

Why this works:

  • AES-256-GCM combines confidentiality (AES-256) with integrity (Galois/Counter Mode) in one operation
  • 256-bit key requires 2^256 trials to brute-force - computationally infeasible
  • Node.js crypto module uses battle-tested OpenSSL implementations
  • GCM's 128-bit authentication tag detects any modification to ciphertext, IV, or AAD
  • Decryption fails immediately if data is tampered with
  • 12-byte IV (96 bits) is GCM-recommended size, must be unique per encryption
  • crypto.randomBytes(12) provides OS-level cryptographic randomness
  • Authenticated Associated Data ("SSN_ENCRYPTION_V1") binds version info to ciphertext
  • Prevents moving ciphertexts between contexts (e.g., swapping encrypted SSNs)
  • Pattern bundles IV + auth tag + ciphertext for synchronized storage
  • Attackers cannot modify or forge ciphertexts without the encryption key

Bcrypt for Password Hashing

const bcrypt = require('bcrypt');
const express = require('express');

class SecurePasswordService {
  constructor() {
    // Cost factor 12 = 2^12 = 4096 iterations
    this.saltRounds = 12;
  }

  async hashPassword(password) {
    this.validatePasswordStrength(password);

    // Bcrypt automatically generates salt
    return await bcrypt.hash(password, this.saltRounds);
  }

  async verifyPassword(password, hash) {
    // Timing-safe comparison
    return await bcrypt.compare(password, hash);
  }

  validatePasswordStrength(password) {
    if (password.length < 12) {
      throw new Error('Password must be at least 12 characters');
    }
    if (!/[A-Z]/.test(password)) {
      throw new Error('Password must contain uppercase letter');
    }
    if (!/[a-z]/.test(password)) {
      throw new Error('Password must contain lowercase letter');
    }
    if (!/\d/.test(password)) {
      throw new Error('Password must contain digit');
    }
  }
}

// Short usage
const passwordService = new SecurePasswordService();
const hash = await passwordService.hashPassword('Str0ngPassw0rd!');
const ok = await passwordService.verifyPassword('Str0ngPassw0rd!', hash);

Why this works:

  • Bcrypt is intentionally slow - makes brute-force attacks economically infeasible
  • Cost factor 12 = 2^12 (4,096) iterations, adjustable as hardware improves
  • Unlike SHA-256 (millions/sec on GPUs), bcrypt's memory-hard algorithm resists parallelization
  • bcrypt.hash() auto-generates 128-bit random salt per password
  • Prevents rainbow table attacks and identical passwords producing same hash
  • Salt + hash combined in one string: $2b$12$[salt][hash]
  • No separate salt storage needed
  • bcrypt.compare() uses constant-time comparison - prevents timing attacks
  • Dummy hash comparison for non-existent users prevents username enumeration
  • Async implementation avoids blocking Node.js event loop
  • Generic error messages don't reveal whether username or password is wrong

Argon2 for Password Hashing (Most Secure)

const argon2 = require('argon2');
const { randomBytes } = require('crypto');

class SecureArgon2Service {
  constructor() {
    this.options = {
      type: argon2.argon2id, // Hybrid mode (best security)
      memoryCost: 65536, // 64 MB
      timeCost: 3, // 3 iterations
      parallelism: 4 // 4 threads
    };
  }

  async hashPassword(password) {
    return await argon2.hash(password, this.options);
  }

  async verifyPassword(password, hash) {
    try {
      return await argon2.verify(hash, password);
    } catch (error) {
      return false;
    }
  }

  async needsRehash(hash) {
    return argon2.needsRehash(hash, this.options);
  }
}

// Short usage
const argon2Service = new SecureArgon2Service();
const hash = await argon2Service.hashPassword('Str0ngPassw0rd!');
const ok = await argon2Service.verifyPassword('Str0ngPassw0rd!', hash);

Why this works:

  • Argon2id won 2015 Password Hashing Competition - current industry best practice
  • Hybrid mode combines Argon2i (side-channel resistant) + Argon2d (GPU resistant)
  • "id" variant recommended for password hashing with maximum attack resistance
  • 64 MB memory cost makes GPU attacks expensive (limited GPU memory bandwidth)
  • Attackers must allocate 64 MB per parallel guess - drastically limits parallelization
  • Time cost (3 iterations) + parallelism (4 threads) balance security vs performance
  • Key innovation over bcrypt: memory requirements prevent GPU cracking
  • argon2.verify() auto-extracts parameters from stored hash
  • argon2.needsRehash() detects outdated parameters
  • Auto-rehashing during login upgrades database without forcing password resets
  • Hash includes all parameters + salt - no additional storage needed

PBKDF2 with High Iteration Count

const crypto = require('crypto');
const { promisify } = require('util');

const pbkdf2Async = promisify(crypto.pbkdf2);

class SecurePBKDF2Service {
  constructor() {
    this.iterations = 600000; // NIST recommendation
    this.keylen = 32; // 256 bits
    this.digest = 'sha256';
  }

  async hashPassword(password) {
    // Generate random salt
    const salt = crypto.randomBytes(32);

    // Derive key
    const hash = await pbkdf2Async(
      password,
      salt,
      this.iterations,
      this.keylen,
      this.digest
    );

    // Combine salt and hash for storage
    return {
      hash: hash.toString('base64'),
      salt: salt.toString('base64'),
      iterations: this.iterations,
      digest: this.digest
    };
  }

  async verifyPassword(password, stored) {
    const salt = Buffer.from(stored.salt, 'base64');

    const hash = await pbkdf2Async(
      password,
      salt,
      stored.iterations,
      this.keylen,
      stored.digest
    );

    const storedHash = Buffer.from(stored.hash, 'base64');

    // Timing-safe comparison
    return crypto.timingSafeEqual(hash, storedHash);
  }

  encodeForStorage(hashedPassword) {
    return JSON.stringify(hashedPassword);
  }

  decodeFromStorage(encoded) {
    return JSON.parse(encoded);
  }
}

// Short usage
const pbkdf2Service = new SecurePBKDF2Service();
const stored = await pbkdf2Service.hashPassword('Str0ngPassw0rd!');
const ok = await pbkdf2Service.verifyPassword('Str0ngPassw0rd!', stored);

Why this works:

  • 600,000 iterations meets NIST SP 800-63B recommendation
  • Requires 600,000 HMAC-SHA256 operations per password guess
  • Multiplies attack cost - transforms hours into years of computation
  • 256-bit random salt (32 bytes) ensures cryptographic uniqueness per password
  • Prevents rainbow table attacks and precomputation
  • Salt stored with hash but doesn't need secrecy - purpose is uniqueness
  • SHA-256 PRF (not deprecated SHA-1) ensures cryptographic strength
  • crypto.timingSafeEqual() examines every byte regardless of differences
  • Prevents timing attacks measuring response times to learn hash info
  • promisify() enables clean async/await code
  • Stores hash + salt + parameters for future upgrades
  • While bcrypt/Argon2 preferred, PBKDF2 remains secure and widely standardized

HMAC-SHA256 for Message Authentication

const crypto = require('crypto');

class SecureHMACService {
  constructor(secret) {
    if (!secret || secret.length < 32) {
      throw new Error('HMAC secret must be at least 32 bytes');
    }
    this.secret = secret;
    this.algorithm = 'sha256';
    this.tolerance = 300; // 5 minutes in seconds
  }

  static generateSecret() {
    return crypto.randomBytes(32);
  }

  generateSignature(data, timestamp) {
    const message = `${timestamp}.${data}`;

    return crypto
      .createHmac(this.algorithm, this.secret)
      .update(message)
      .digest('hex');
  }

  verifySignature(data, signature, timestamp) {
    // Check timestamp (replay protection)
    const currentTime = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTime - timestamp) > this.tolerance) {
      return false;
    }

    // Compute expected signature
    const expected = this.generateSignature(data, timestamp);

    // Timing-safe comparison
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }
}

// Short usage
const hmac = new SecureHMACService(SecureHMACService.generateSecret());
const timestamp = Math.floor(Date.now() / 1000);
const sig = hmac.generateSignature('payload', timestamp);
const ok = hmac.verifySignature('payload', sig, timestamp);

Why this works:

  • HMAC construction combines secret key + data via hash function
  • Inner/outer padding with XOR prevents length extension attacks
  • Impossible to forge signatures without secret key, even observing many valid pairs
  • SHA-256 provides 256-bit security vs collision/preimage attacks
  • Avoids vulnerabilities in deprecated SHA-1 or MD5
  • Secret key must be ≥32 bytes (256 bits) to match SHA-256 security
  • crypto.randomBytes(32) generates cryptographically secure keys from OS entropy
  • Timestamp inclusion limits signature validity to 5-minute window
  • Prevents replay attacks - intercepted messages expire after tolerance window
  • crypto.timingSafeEqual() constant-time comparison prevents timing attacks
  • Suitable for webhooks, API signing, and message authentication

Scrypt for Key Derivation

const crypto = require('crypto');
const { promisify } = require('util');

const scryptAsync = promisify(crypto.scrypt);

class SecureScryptService {
  constructor() {
    this.keyLength = 32; // 256 bits
    this.saltLength = 32; // 256 bits
    this.options = {
      N: 16384, // CPU/memory cost (2^14)
      r: 8,     // Block size
      p: 1,     // Parallelization
      maxmem: 32 * 1024 * 1024 // 32 MB
    };
  }

  async deriveKey(password) {
    const salt = crypto.randomBytes(this.saltLength);

    const key = await scryptAsync(
      password,
      salt,
      this.keyLength,
      this.options
    );

    return {
      key: key.toString('base64'),
      salt: salt.toString('base64'),
      ...this.options
    };
  }

  async verifyKey(password, stored) {
    const salt = Buffer.from(stored.salt, 'base64');

    const key = await scryptAsync(
      password,
      salt,
      this.keyLength,
      {
        N: stored.N,
        r: stored.r,
        p: stored.p,
        maxmem: stored.maxmem
      }
    );

    const storedKey = Buffer.from(stored.key, 'base64');

    return crypto.timingSafeEqual(key, storedKey);
  }
}

// Short usage
const scryptService = new SecureScryptService();
const derived = await scryptService.deriveKey('StrongPassword!');
const ok = await scryptService.verifyKey('StrongPassword!', derived);

Why this works:

  • Scrypt resists GPU/ASIC/FPGA attacks by requiring large memory + computation
  • N=16384 (2^14) requires ~16 MB memory per derivation
  • Parallel attacks expensive - each attempt needs dedicated RAM
  • Unlike bcrypt, cannot parallelize easily on GPUs despite computational expense
  • Generates large pseudorandom vector requiring fast memory + random access
  • Forces attackers to either allocate full memory (expensive) or recompute (slow)
  • r=8 (block size) and p=1 (parallelization) balance security vs performance
  • Unique random salt per derivation - identical passwords produce different keys
  • Derived 256-bit key used with AES-256-GCM for authenticated encryption
  • Scrypt parameters + salt stored with ciphertext for decryption
  • Node.js built-in crypto.scrypt() provides native performance
  • Well-suited for password-based encryption vs password storage

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

Additional Resources