Hallucination Attacks in Express with Hmac Signatures
Hallucination Attacks in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A hallucination attack in an Express API that uses Hmac signatures occurs when an attacker provides a valid signature for a request body or parameters that do not match the expected resource state, causing the server to act on fabricated data. This can lead to unauthorized operations, such as changing a user’s email or escalating privileges, because the server trusts the signature without verifying that the underlying data is consistent with the server’s source of truth.
In Express, developers often compute an Hmac over incoming JSON (e.g., the request body or selected fields) using a shared secret and then compare it to a signature provided in a header (commonly x-hmac-sha256 or similar). If the server computes the Hmac over the received payload and it matches the client-supplied signature, the request is considered authentic. However, if the server processes or normalizes the JSON differently between signature verification and business logic (for example, re-parsing the body with req.body after middleware like express.json()), subtle mismatches can occur. These mismatches enable hallucination: an attacker can supply a payload that produces the same Hmac under a crafted but benign-looking structure, while the server later interprets the data in an unintended way.
Consider an endpoint that transfers funds based on JSON fields {from, to, amount}. The client computes the Hmac over the exact JSON string sent. If the server verifies the Hmac successfully but then applies business rules that coerce or override fields (for instance, defaulting missing numeric values, merging partial updates, or applying type coercion), the effective operation may diverge from what the Hmac was computed over. This divergence is the vulnerability: the Hmac ensures integrity of the signed bytes, but not semantic integrity of the business objects. Attackers can hallucinate transfers or modifications by exploiting normalization steps, missing validation, or inconsistent serialization (such as omitting fields that are added server-side).
Real-world patterns that increase risk include accepting JSON with polymorphic types, optional fields that change meaning when absent, and endpoints that perform multiple logical operations based on a single signature. Without explicit checks that the data used for authorization and business logic is identical to the data over which the Hmac was computed, Express APIs remain exposed to hallucination. This is especially relevant when relying solely on Hmac for authentication/authorization without additional context binding (e.g., tying the signature to a specific resource version or tenant ID).
Hmac Signatures-Specific Remediation in Express — concrete code fixes
To mitigate hallucination attacks, ensure the data over which you compute and verify the Hmac is exactly the data used in business logic, and bind the signature to request context. Below are concrete Express patterns and code examples that reduce risk.
1. Use a strict body hashing approach with raw buffers
Avoid re-parsing JSON after reading the raw body; compute the Hmac on the raw bytes that the client signed. Use crypto.createHmac with a constant-time compare to prevent timing attacks.
const crypto = require('crypto');
const express = require('express');
const app = express();
// Middleware to capture raw body for Hmac verification
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
function verifyHmac(req, res, next) {
const signature = req.get('x-hmac-sha256');
if (!signature) return res.status(401).send('Missing signature');
const hmac = crypto.createHmac('sha256', process.env.HMAC_SECRET);
hmac.update(req.rawBody);
const expected = 'sha256=' + hmac.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send('Invalid signature');
}
next();
}
app.post('/transfer', verifyHmac, (req, res) => {
const data = JSON.parse(req.rawBody.toString('utf8'));
// Use data directly without additional normalization that could diverge
if (typeof data.amount !== 'number' || data.amount <= 0) {
return res.status(400).send('Invalid amount');
}
// Perform transfer using data.from, data.to, data.amount
res.send('OK');
});
2. Bind signature to resource and context
Include a resource identifier (e.g., record ID or version) and tenant/context in the signed payload to prevent cross-resource hallucination. Require the server to recompute the Hmac over the exact same context-bound payload.
// Client-side construction example (Node)
const crypto = require('crypto');
const payload = {
resource: '/accounts/123',
action: 'update-email',
email: '[email protected]',
ts: Math.floor(Date.now() / 1000)
};
const payloadStr = JSON.stringify(payload);
const hmac = crypto.createHmac('sha256', process.env.HMAC_SECRET);
const signature = 'sha256=' + hmac.update(payloadStr).digest('hex');
// Send payloadStr as body and signature in x-hmac-sha256
// Server-side verification
function verifyWithContext(req, res, next) {
const signature = req.get('x-hmac-sha256');
const bodyStr = req.rawBody.toString('utf8');
const hmac = crypto.createHmac('sha256', process.env.HMAC_SECRET);
hmac.update(bodyStr);
const expected = 'sha256=' + hmac.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(bodyStr);
if (data.resource !== req.path) {
return res.status(400).send('Resource mismatch');
}
next();
}
3. Validate and normalize before use; reject unexpected fields
After signature verification, parse the JSON and validate strictly. Do not add default fields that would change the semantics of the signed payload. Use a schema validator and reject any fields not explicitly allowed.
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: 'object',
required: ['resource', 'action', 'email'],
properties: {
resource: { const: '/accounts/{id}' },
action: { const: 'update-email' },
email: { type: 'string', format: 'email' },
ts: { type: 'integer' }
},
additionalProperties: false
};
const validate = ajv.compile(schema);
app.post('/transfer', verifyHmac, (req, res) => {
const data = JSON.parse(req.rawBody.toString('utf8'));
if (!validate(data)) return res.status(400).send('Invalid payload');
// Proceed with action using validated data
res.send('OK');
});
4. Use idempotency keys and server-side reconciliation
Store a hash of the signed payload server-side for a short window and reject replays or mismatched interpretations. This ensures that even if hallucination is attempted via timing or race conditions, the server maintains consistent state.
const seen = new Set();
app.post('/transfer', verifyHmac, (req, res) => {
const data = JSON.parse(req.rawBody.toString('utf8'));
const idKey = data.idempotencyKey;
if (seen.has(idKey)) return res.status(409).send('Duplicate request');
seen.add(idKey);
// Process transfer
res.send('OK');
});
Related CWEs: llmSecurity
| CWE ID | Name | Severity |
|---|---|---|
| CWE-754 | Improper Check for Unusual or Exceptional Conditions | MEDIUM |