Topic: mcp server weak cryptography security

MCP server weak cryptography security — MD5, ECB mode, short keys, and weak KDF

Weak cryptography in MCP servers is common because most servers are written by developers who reach for the first crypto API they find — and the first API found is often a legacy one. MD5 and SHA-1 are still used for "checksums" that are in fact integrity verifications where collision resistance matters. ECB mode appears in AES implementations because it's the easiest to reach. PBKDF2 with 1,000 iterations and MD5 as the hash is still generated by some scaffolding tools. Each of these is detectable in static analysis and each has a straightforward fix.

Pattern 1: MD5 and SHA-1 used for integrity checks

MD5 is broken: chosen-prefix collision attacks can produce two different files with the same MD5 hash in under a minute on consumer hardware. SHA-1 is broken for theoretical collision: SHAttered (2017) demonstrated a practical collision. Using either for integrity verification means an attacker can substitute a malicious file and produce a matching hash.

In MCP servers, MD5/SHA-1 appear most often as: a "fingerprint" of a tool definition to detect changes, a cache key for a computation result, a checksum of a downloaded file before parsing, or an HMAC substitute for request signing.

const crypto = require('crypto');

// WRONG: MD5 for a file integrity check
function fileChecksum_WRONG(filepath) {
  const data = fs.readFileSync(filepath);
  return crypto.createHash('md5').update(data).digest('hex');
}

// WRONG: SHA-1 for a request signature
function signRequest_WRONG(payload, secret) {
  return crypto.createHmac('sha1', secret).update(payload).digest('hex');
}

// RIGHT: SHA-256 for content integrity (no authentication needed)
function fileChecksum(filepath) {
  const data = fs.readFileSync(filepath);
  return crypto.createHash('sha256').update(data).digest('hex');
}

// RIGHT: HMAC-SHA256 for authenticated integrity (you need to verify WHO signed it)
function signRequest(payload, secret) {
  return crypto.createHmac('sha256', Buffer.from(secret, 'hex')).update(payload).digest('hex');
}

// RIGHT: constant-time comparison when verifying HMAC to prevent timing attacks
function verifyHmac(payload, secret, receivedHmac) {
  const expected = signRequest(payload, secret);
  // timingSafeEqual requires same-length buffers
  const expectedBuf = Buffer.from(expected, 'hex');
  const receivedBuf = Buffer.from(receivedHmac, 'hex');
  if (expectedBuf.length !== receivedBuf.length) return false;
  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}

Pattern 2: AES-ECB mode

AES-ECB (Electronic Codebook) mode encrypts each 16-byte block independently. Identical plaintext blocks produce identical ciphertext blocks. This means patterns in the plaintext are visible in the ciphertext — the classic demonstration is the ECB penguin, where an encrypted bitmap image still shows the penguin shape. For MCP servers encrypting structured data (JSON payloads, token values), ECB leaks structural information.

// WRONG: AES-ECB — patterns in plaintext visible in ciphertext
function encrypt_WRONG(data, keyHex) {
  const key = Buffer.from(keyHex, 'hex');
  const cipher = crypto.createCipheriv('aes-256-ecb', key, null);
  return Buffer.concat([cipher.update(data), cipher.final()]).toString('hex');
}

// RIGHT: AES-256-GCM — authenticated encryption (confidentiality + integrity)
function encrypt(plaintext, keyHex) {
  const key = Buffer.from(keyHex, 'hex'); // must be 32 bytes for AES-256
  const iv = crypto.randomBytes(12);       // 96-bit IV for GCM
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag(); // 16-byte GCM authentication tag

  // Prepend IV and auth tag so the decrypt function can use them
  return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}

function decrypt(ciphertextB64, keyHex) {
  const key = Buffer.from(keyHex, 'hex');
  const buf = Buffer.from(ciphertextB64, 'base64');

  const iv      = buf.subarray(0, 12);
  const authTag = buf.subarray(12, 28);
  const data    = buf.subarray(28);

  const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
  decipher.setAuthTag(authTag);

  // If authTag doesn't match, this throws — protects against tampered ciphertext
  return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
}

// Key generation: use crypto.randomBytes(32) — never derive from a short password
// without a proper KDF (see Pattern 4 below)
const KEY_HEX = crypto.randomBytes(32).toString('hex');

Pattern 3: short key lengths

MCP servers sometimes use short RSA keys (1024-bit) for signing tokens, or hardcode a short secret for HMAC that's derived from a human-readable string. A 128-bit HMAC key derived from a 10-character password has the effective entropy of the password, not 128 bits.

// WRONG: 1024-bit RSA key (factorable with current hardware)
// const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 1024 });

// WRONG: short HMAC secret derived from a human password directly
// const hmacKey = Buffer.from('mypassword123'); // ~40 bits of entropy

// RIGHT: RSA key generation with current minimum sizes
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 4096, // 2048 minimum; 4096 for new keys
  publicKeyEncoding:  { type: 'spki',  format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});

// RIGHT: ECDSA P-256 (smaller key, faster, equivalent security to RSA-3072)
const { privateKey: ecPrivKey } = crypto.generateKeyPairSync('ec', {
  namedCurve: 'P-256',
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});

// RIGHT: HMAC key with full entropy (for server-generated keys)
const HMAC_KEY = crypto.randomBytes(32); // 256 bits, full entropy — not derived from password

Pattern 4: weak key derivation functions

When a human-supplied password must be converted to a cryptographic key (for encrypting a user's data at rest), the derivation function determines how much work an attacker must do to brute-force the password. PBKDF2 with 1,000 iterations and MD5 as the hash — still seen in scaffolded code — offers roughly the same protection as no KDF at all on modern hardware.

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

// WRONG: PBKDF2 with 1,000 iterations and MD5 — negligible work factor
// crypto.pbkdf2Sync(password, salt, 1000, 32, 'md5')

// WRONG: PBKDF2 with SHA-256 but only 10,000 iterations — below NIST 2023 guidance
// crypto.pbkdf2Sync(password, salt, 10_000, 32, 'sha256')

// RIGHT: scrypt — memory-hard, parallelism-resistant
async function deriveKeyScrypt(password, salt) {
  // N=2^17 (131072), r=8, p=1: ~1 second on typical server hardware
  const derivedKey = await scrypt(password, salt, 32, { N: 131072, r: 8, p: 1 });
  return derivedKey;
}

// Usage: store salt alongside the derived key (salt is not secret)
async function encryptUserData(password, plaintext) {
  const salt = crypto.randomBytes(16);
  const key = await deriveKeyScrypt(password, salt);
  const encrypted = encrypt(plaintext, key.toString('hex'));
  return { salt: salt.toString('hex'), ciphertext: encrypted };
}

// RIGHT: PBKDF2 at current NIST-recommended iteration count (600,000+ for SHA-256)
function deriveKeyPBKDF2(password, salt) {
  return crypto.pbkdf2Sync(password, salt, 600_000, 32, 'sha256');
}

SkillAudit detection

SkillAudit's Security axis includes a dedicated Weak Cryptography check that runs static analysis over all crypto API calls in your MCP server: it flags createHash('md5'), createHash('sha1'), createCipheriv('aes-*-ecb',...), generateKeyPairSync('rsa', { modulusLength: N }) where N < 2048, and pbkdf2Sync/pbkdf2 calls with iteration counts below 600,000. The Credential Exposure axis flags cases where derived keys or ciphertext are logged or returned in tool responses.

Run a SkillAudit scan on your MCP server to identify all weak cryptography patterns before your server appears in a public audit report.


Related: Insecure randomness · Anatomy of a credential leak · Credential exposure security