Command Injection with Jwt Tokens
How Command Injection Manifests in Jwt Tokens
Command injection occurs when attacker‑controlled data is interpreted as part of an operating‑system command. In JWT‑based APIs this often happens when the raw token string is concatenated into a shell command without proper sanitisation or use of safe APIs.
Consider a Node.js endpoint that uses the jsonwebtoken library for verification but then passes the token to child_process.exec:
const jwt = require('jsonwebtoken');
const { exec } = require('child_process');
app.get('/process', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.sendStatus(401);
// Vulnerable: token is placed directly into a shell command
exec(`openssl enc -base64 <<< ${token}`, (err, stdout) => {
if (err) return res.status(500).send(err.message);
res.send(`Decoded: ${stdout}`);
});
});
An attacker who can create a valid JWT (e.g., by knowing the signing secret) can embed shell metacharacters such as ;, &, |, or $(…) inside the token. When the server builds the command string, those characters break out of the intended openssl call and execute arbitrary commands.
A similar pattern appears in Python with PyJWT:
import jwt
import subprocess
token = request.headers.get('Authorization', '').split()[-1]
# Vulnerable: token concatenated into a shell command
output = subprocess.check_output(f'echo {token} | base64', shell=True)
return output
In both examples the token is treated as data, but the code incorrectly places it inside a command string, enabling command injection.
Jwt Tokens-Specific Detection
middleBrick performs unauthenticated, black‑box scanning. For command injection it sends a series of payloads that contain typical shell metacharacters inside the JWT value placed in the Authorization: Bearer header. Example payloads include:
; id| cat /etc/passwd$(whoami)& sleep 5
If the endpoint reflects the output of the injected command (e.g., returns the result of id or shows a delayed response from sleep), middleBrick flags the finding as command injection with a severity rating based on the impact observed (data exfiltration, remote code execution, etc.).
The scanner also looks for indirect signs such as error messages that reveal shell syntax errors or changes in response timing, which can indicate blind command injection. Because middleBrick does not require any agents or credentials, it can test the exact same code path that a real attacker would use: a token supplied by the client.
Note that middleBrick only reports the issue; it does not attempt to exploit it beyond confirming that the payload caused a detectable change in the response.
Jwt Tokens-Specific Remediation
The fix is to never place the raw JWT (or any user‑controlled string) into a shell command. Instead, verify the token first and then use the verified claims as data. If a command must be run, avoid constructing a command string; use APIs that accept arguments separately.
Here is the corrected Node.js version:
const jwt = require('jsonwebtoken');
const { execFile } = require('child_process');
app.get('/process', (req, res) => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) return res.sendStatus(401);
const token = auth.slice(7);
try {
// Verify the token – rejects any tampered value
const payload = jwt.verify(token, process.env.JWT_SECRET);
// Use payload data, not the raw token, for any further logic
// Example: run a fixed command with the token passed as data via stdin
execFile('openssl', ['enc', '-base64'], { input: token }, (err, stdout) => {
if (err) return res.status(500).send(err.message);
res.send(`Decoded: ${stdout}`);
});
} catch (err) {
return res.status(401).send('Invalid token');
}
});
Key changes:
- The token is verified with
jwt.verifybefore any use. execFileis used instead ofexec; the command and its arguments are supplied as an array, so the token is never interpreted by a shell.- The token is passed via the
inputoption, treating it purely as data.
The equivalent safe Python example:
import jwt
import subprocess
token = request.headers.get('Authorization', '').split()[-1]
try:
payload = jwt.decode(token, options={"verify_signature": True}, key=YOUR_SECRET)
# Use subprocess without shell=True; pass token as stdin data
result = subprocess.run(['openssl', 'enc', '-base64'], input=token.encode(), capture_output=True, text=True)
return result.stdout
except jwt.InvalidTokenError:
return 'Invalid token', 401
By verifying the token and avoiding shell concatenation, the command injection vector is eliminated while preserving the intended functionality.
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 |
Frequently Asked Questions
Can middleBrick detect command injection if the vulnerability only appears after a valid JWT is verified?
Is it safe to use a JWT token directly in a shell command if I first verify its signature?
; or $(…). When that token is concatenated into a command string, those characters are still interpreted by the shell, leading to command injection. The proper defense is to avoid placing the token in a command context altogether or to use safe APIs that treat the token as data (e.g., execFile with argument arrays or passing the token via stdin).