Crlf Injection with Jwt Tokens
How CRLF Injection Manifests in JWT Tokens
Cross‑line (CRLF) injection occurs when an attacker can inject carriage‑return (%0D) and line‑feed (%0A) characters into data that is later written into an HTTP header. In JWT‑based authentication the most common vector is the Authorization: Bearer <token> header. If any part of the JWT (usually a claim such as sub, username, or email) is taken directly from user input without validation, an attacker can supply a value containing %0D%0A. After the server decodes the URL‑encoding, the newline characters become part of the token string, and when the token is concatenated into the header the sequence splits the header, allowing the attacker to inject arbitrary headers (e.g., Set‑Cookie or Location) or to perform HTTP response splitting.
Typical vulnerable code paths include:
- Login endpoints that echo a JWT back in a response header after signing it with user‑supplied data.
- Token refresh or introspection routes where the JWT is reflected in a
WWW‑AuthenticateorLinkheader. - API gateways that copy the
Authorizationheader from the request into a response header for logging or debugging purposes.
Real‑world examples have been recorded in CVEs such as CVE-2020-13944 (Apache Tomcat HTTP Response Splitting) where insufficient validation of user‑controlled values led to header injection. When the injected value is a JWT, the attacker can also attempt to manipulate claims (e.g., elevate privileges) while simultaneously achieving response splitting.
JWT Tokens-Specific Detection
Detecting CRLF injection in JWT contexts requires checking whether user‑controlled input that ends up inside a JWT is later reflected in an HTTP header without proper sanitisation. middleBrick’s Input Validation check performs active probing by sending payloads that contain URL‑encoded CRLF sequences (%0D%0A) in query parameters, POST bodies, or cookies that are known to be used in JWT construction. The scanner then examines the response for the presence of the injected header (e.g., a custom X-Injected: true line) or for abnormal header splitting patterns.
Example of a probing request that middleBrick might generate:
GET /login?username=admin%0d%0aX-Injected%3A+true HTTP/1.1
Host: example.com
If the server reflects the username inside a JWT that is placed in an Authorization header, the response will contain:
HTTP/1.1 200 OK
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbibi...%0D%0aX-Injected%3A+true
...The appearance of the CRLF‑separated line indicates a successful injection. middleBrick reports this as a high‑severity finding under the Input Validation category, providing the exact payload, the reflected header, and remediation guidance.
In addition to active probing, middleBrick parses any OpenAPI/Swagger specification supplied with the scan. It cross‑references parameters that are defined as strings and used in security schemes (e.g., apiKey in header) with the runtime findings, highlighting which API operations are susceptible to CRLF injection via JWT claims.
JWT Tokens-Specific Remediation
The root cause is insufficient validation of data that becomes part of a JWT claim before the token is inserted into an HTTP header. Remediation therefore focuses on strict input validation and safe construction of headers.
Node.js (jsonwebtoken)
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
function isSafeUsername(str) {
// Allow only alphanumerics, underscore and hyphen
return /^[A-Za-z0-9_-]+$/.test(str);
}
app.get('/login', (req, res) => {
const username = req.query.username || '';
if (!isSafeUsername(username)) {
return res.status(400).send({ error: 'Invalid username' });
}
const token = jwt.sign({ sub: username }, 'secret', { expiresIn: '1h' });
// Safe: token is already validated, no user‑controlled characters remain
res.setHeader('Authorization', `Bearer ${token}`);
res.json({ token });
});
app.listen(3000);
The isSafeUsername function rejects any input containing control characters, spaces, or punctuation that could be used for CRLF injection.
Python (PyJWT)
import jwt
from flask import Flask, request, make_response, abort
app = Flask(__name__)
SAFE_USERNAME_PATTERN = r'^[A-Za-z0-9_-]+$'
import re
def is_safe_username(username):
return re.match(SAFE_USERNAME_PATTERN, username) is not None
@app.route('/login')
def login():
username = request.args.get('username', '')
if not is_safe_username(username):
abort(400, description='Invalid username')
payload = {'sub': username}
token = jwt.encode(payload, 'secret', algorithm='HS256')
resp = make_response({'token': token})
resp.headers['Authorization'] = f'Bearer {token}'
return resp
if __name__ == '__main__':
app.run(port=5000)
Here the validation step occurs before the JWT is created, ensuring that the claim cannot contain %0D%0A or any other dangerous characters.
Additional defensive measures:
- Never concatenate raw user input into header values; always use the language’s HTTP library which will percent‑encode or reject invalid characters.
- If reflecting a JWT in a header is unavoidable, encode the token with Base64URL (the JWT already is) and verify that the header value does not contain
\ror\nafter construction. - Deploy a middleware that scans outgoing headers for CRLF sequences and strips or rejects them as a safety net.
By applying these fixes, the API no longer reflects attacker‑controlled newlines, eliminating the CRLF injection vector while preserving legitimate JWT functionality.
Frequently Asked Questions
Can CRLF injection in a JWT lead to account takeover?
Authorization header, they can add arbitrary headers such as Set‑Cookie: session=attacker. The victim’s browser or downstream client may then accept the attacker’s cookie or be redirected to a malicious site, enabling session hijacking or phishing. The vulnerability does not directly alter the JWT signature, but the resulting HTTP response splitting can be leveraged to steal or manipulate sessions.