Unicode Normalization in Express with Api Keys
Unicode Normalization in Express with Api Keys — how this combination creates or exposes the vulnerability
Unicode normalization inconsistencies in Express applications become high risk when API keys are handled as user-controlled input or as identifiers derived from normalized strings. An API key is often treated as an opaque string, but if the application normalizes incoming data differently than the storage layer, an attacker can supply a visually identical key that maps to a different internal representation, bypassing intended access controls.
For example, the Unicode character é can be represented as a single code point U+00E9 or as a decomposed sequence e + combining acute accent (U+0301). If an API key is stored in a normalized form (NFC) but the Express route does not normalize incoming keys before comparison, a request with the decomposed form may be treated as a distinct key. This leads to authentication bypass or privilege confusion, which is surfaced by the Authentication and Property Authorization checks in middleBrick scans.
In Express, routes and parameter handling do not automatically normalize strings. If keys are passed in headers or URL paths, the application must explicitly normalize them. Attack patterns such as IDOR may be chained: an attacker iterates over visually equivalent key forms to enumerate valid keys. middleBrick tests for these inconsistencies by comparing normalized definitions from an OpenAPI spec against runtime behavior, including how authorization checks apply to key-like parameters.
When API keys are stored in databases or configuration files, ensure a consistent normalization strategy and apply it before comparison. middleBrick’s OpenAPI/Swagger analysis resolves $ref definitions and cross-references them with runtime findings, highlighting mismatches in how keys are expected versus how they are accepted. This is particularly important for endpoints that accept keys as path parameters, query strings, or custom headers in Express routes.
Api Keys-Specific Remediation in Express — concrete code fixes
To secure API key handling in Express, normalize all incoming key values using the same Unicode form used during storage, and perform constant-time comparisons to avoid timing leaks. Below are concrete, syntactically correct examples demonstrating a robust approach.
1. Normalize and compare keys consistently
Use the built-in normalize method from the Unicode standard and a constant-time comparison utility to avoid leaking information through timing differences.
const express = require('express');
const crypto = require('crypto');
const app = express();
// Normalize to NFC and compare in constant time
function safeKeyCompare(a, b) {
const normalizedA = a.normalize('NFC');
const normalizedB = b.normalize('NFC');
return crypto.timingSafeEqual(
Buffer.from(normalizedA),
Buffer.from(normalizedB)
);
}
const VALID_KEYS = new Set([
'2AbC3dEf4GhIjKlMnOp5QrStUvWxYz'.normalize('NFC'),
'9XyZ8qRsTuVvWwXxYyZz0AaBbCcDd'.normalize('NFC')
]);
app.get('/resource/:providedKey', (req, res) => {
const provided = req.params.providedKey;
let authorized = false;
for (const key of VALID_KEYS) {
if (safeKeyCompare(provided, key)) {
authorized = true;
break;
}
}
if (!authorized) {
return res.status(401).json({ error: 'Unauthorized' });
}
res.json({ data: 'protected' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
2. Normalize keys from headers and query parameters
When keys are provided via headers or query strings, normalize them before use and avoid mixing representations in routing logic.
const express = require('express');
const crypto = require('crypto');
const app = express();
function normalizeAndValidate(apiKeyHeader) {
if (!apiKeyHeader) return null;
return apiKeyHeader.normalize('NFC');
}
// Example set of stored, pre-normalized keys
const storedKeys = new Set([
'AbC123'.normalize('NFC'),
'XyZ789'.normalize('NFC')
]);
app.use((req, res, next) => {
const raw = req.get('X-API-Key') || req.query.api_key;
const key = normalizeAndValidate(raw);
if (!key) return res.status(400).json({ error: 'Missing key' });
if (!storedKeys.has(key)) return res.status(403).json({ error: 'Forbidden' });
req.apiKey = key;
next();
});
app.get('/secure', (req, res) => {
res.json({ key: req.apiKey, message: 'Authorized' });
});
app.listen(3000, () => console.log('Server running on port 3030'));
3. Use middleware for centralized key validation
Encapsulate normalization and validation in reusable middleware to ensure consistent behavior across routes and reduce the risk of accidental bypass.
const express = require('express');
const crypto = require('crypto');
const app = express();
const VALID_KEYS = new Set([
'KeyOne'.normalize('NFC'),
'KeyTwo'.normalize('NFC')
]);
function apiKeyAuth(req, res, next) {
const source = req.get('Authorization') || req.query.api_key;
if (!source) return res.status(401).json({ error: 'Authorization header or query parameter required' });
const normalized = source.normalize('NFC');
const isValid = Array.from(VALID_KEYS).some(k => crypto.timingSafeEqual(
Buffer.from(normalized),
Buffer.from(k)
));
if (!isValid) return res.status(403).json({ error: 'Invalid key' });
req.userKey = normalized;
next();
}
app.get('/items', apiKeyAuth, (req, res) => {
res.json({ items: ['a', 'b'], key: req.userKey });
});
app.post('/items', apiKeyAuth, express.json(), (req, res) => {
res.status(201).json({ received: req.body, key: req.userKey });
});
app.listen(3000, () => console.log('Server running on port 3000'));
These patterns ensure that Unicode-equivalent keys are treated identically and that comparisons do not leak timing information. For ongoing assurance, integrate middleBrick’s CLI or GitHub Action to scan your Express endpoints; the scans include checks for Authentication, Property Authorization, and input handling related to key-like parameters, with findings mapped to OWASP API Top 10 and compliance frameworks.