Sql Injection in Express with Hmac Signatures
Sql Injection in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
SQL injection in Express applications that rely on HMAC signatures for request integrity can still occur when signatures are validated without verifying the trustworthiness and correctness of the incoming data used in query construction. A common pattern is to sign a subset of request parameters (e.g., an identifier or filter value) using a shared secret and include the signature in headers such as X-API-Signature. If the server validates the HMAC to confirm the parameter has not been tampered with but then directly interpolates that parameter into a SQL string, the signature merely confirms that the attacker provided a valid signature for their chosen input, not that the input is safe.
Consider an Express endpoint that uses query parameters for filtering and signs the filter value. An attacker can supply any value, and as long as they provide a correct HMAC (e.g., computed with the known secret), the server treats the value as trusted. If the server uses the unsigned value directly in a SQL query—such as concatenating it into a WHERE clause—the signature does nothing to prevent injection; it only ensures the value hasn’t been altered in transit, not that it is safe. This situation commonly arises when developers assume integrity implies safety, leading to vulnerabilities tied to improper validation and unsafe SQL construction.
Real-world examples include endpoints that build queries using template literals or string concatenation with user-controlled data that has an HMAC, such as SELECT * FROM products WHERE category = '${category}', where category is signed but not sanitized or parameterized. Attackers can exploit this by providing payloads like A' OR '1'='1 with a valid HMAC, potentially bypassing authentication checks or extracting data. The root cause is treating HMAC-protected input as sanitized input, neglecting parameterized queries and strict allowlists, which enables classic SQL injection despite the presence of cryptographic integrity checks.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
To remediate SQL injection risks in Express when using HMAC signatures, you must treat signed data as untrusted for SQL construction and enforce strict input validation and parameterized queries. HMACs are useful for ensuring data integrity and detecting tampering, but they do not replace safe data handling practices. Always validate the format and type of the input before using it, and use parameterized queries or an ORM that enforces separation between code and data.
Example: Unsafe usage with HMAC
const express = require('express');
const crypto = require('crypto');
const app = express();
const SHARED_SECRET = 'super-secret-key';
function verifySignature(value, signature) {
const hmac = crypto.createHmac('sha256', SHARED_SECRET);
hmac.update(value);
return hmac.digest('hex') === signature;
}
app.get('/products', (req, res) => {
const { category, sig } = req.query;
if (!category || !sig || !verifySignature(category, sig)) {
return res.status(400).send('Invalid signature');
}
// Unsafe: concatenating user input into SQL even though it is HMAC-signed
const sql = `SELECT * FROM products WHERE category = '${category}'`;
db.query(sql, (err, results) => {
if (err) return res.status(500).send('Error');
res.json(results);
});
});
Example: Safe remediation with parameterized queries
const express = require('express');
const crypto = require('crypto');
const app = express();
const SHARED_SECRET = 'super-secret-key';
function verifySignature(value, signature) {
const hmac = crypto.createHmac('sha256', SHARED_SECRET);
hmac.update(value);
return crypto.timingSafeEqual(Buffer.from(hmac.digest('hex')), Buffer.from(signature));
}
app.get('/products', (req, res) => {
const { category, sig } = req.query;
if (!category || !sig || !verifySignature(category, sig)) {
return res.status(400).send('Invalid signature');
}
// Validate format: only allow alphanumeric and common category characters
if (!/^[a-zA-Z0-9_\-\s]+$/.test(category)) {
return res.status(400).send('Invalid category format');
}
// Safe: parameterized query ensures user input is never interpreted as SQL
const sql = 'SELECT * FROM products WHERE category = ?';
db.query(sql, [category], (err, results) => {
if (err) return res.status(500).send('Error');
res.json(results);
});
});
Additional best practices
- Use an allowlist for expected values where feasible (e.g., known category IDs or slugs).
- Prefer using an ORM or query builder that enforces parameterization by default.
- Use
crypto.timingSafeEqualfor signature comparisons to prevent timing attacks. - Log invalid signature attempts for monitoring, but do not expose internal SQL errors to clients.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |