Command Injection in Laravel with Jwt Tokens
Command Injection in Laravel with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Command injection occurs when an attacker can inject and execute arbitrary system commands. In Laravel, this risk can intersect with JWT token handling when token data is used to build shell commands. Consider a scenario where a decoded JWT claim (for example, a username or hostname) is passed to a system call without validation or escaping:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class ReportController extends Controller
{
public function export(Request $request)
{
$token = $request->header('Authorization');
if (!$token) {
return response('Unauthorized', 401);
}
// Insecure: assumes payload is safe
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS256'));
$username = $decoded->username; // attacker-controlled if token is compromised or algorithm is weak
// Dangerous: user input embedded in shell command
$output = shell_exec("tar -czf /tmp/report-{$username}.tar.gz /var/reports");
return response($output);
}
}
</code>
In this example, if an attacker can craft a valid JWT (by stealing the secret, exploiting a weak algorithm like none, or brute-forcing the secret), they can control the username claim. If the application trusts the token’s structure, the attacker can inject shell metacharacters (e.g., ;, &, |, `, or $()) into the username to achieve command injection. The JWT itself becomes a vector because the token’s payload is used to construct a shell command without sanitization or strict allowlisting.
Additionally, JWT tokens may carry roles or permissions that, if improperly trusted, can lead to privilege escalation when combined with command execution. For instance, an attacker who modifies a role claim to an administrative value might trigger a code path that runs high-privilege shell commands. Even if the token is cryptographically valid, the application must treat all decoded data as untrusted input. The risk is not only in direct command construction but also in logging or monitoring features that invoke shell utilities on token-derived values.
Other indirect patterns include using JWT data to select files or directories for archival or diagnostic commands. If a filename or path derived from a JWT claim is used in functions like exec, system, or backtick operators, and the input is not rigorously validated, command injection can occur. Attackers may also exploit weak secret management to forge tokens that enable these dangerous code paths.
Jwt Tokens-Specific Remediation in Laravel — concrete code fixes
Remediation focuses on never passing untrusted JWT claims directly to shell commands. Use strict allowlisting, avoid shell execution where possible, and apply defense-in-depth validation.
- Validate and sanitize all JWT-derived data: Treat decoded claims as untrusted. Use allowlists for known values instead of blacklists.
- Avoid shell execution entirely: Prefer PHP-native operations (e.g.,
ZipArchivefor archives) over shell commands. - Use escapeshellarg: If shell commands are unavoidable, escape arguments properly.
Example 1: Safe filename construction
Instead of injecting a raw claim into a shell command, validate the username against a safe pattern and use PHP to create the archive:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use ZipArchive;
class ReportController extends Controller
{
public function export(Request $request)
{
$token = $request->header('Authorization');
if (!$token) {
return response('Unauthorized', 401);
}
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS256'));
$username = $decoded->username;
// Strict allowlist: only alphanumeric, underscore, hyphen (adjust as needed)
if (!preg_match('/^[a-zA-Z0-9_-]+$/D', $username)) {
return response('Invalid username', 400);
}
// Safe: use PHP to create archive, no shell involvement
$zip = new ZipArchive();
$filename = storage_path("app/reports/report-{$username}.zip");
if ($zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return response('Cannot create archive', 500);
}
$zip->addFile('/var/reports/data.txt', 'report.txt');
$zip->close();
return response()->download($filename);
}
}
</code>
Example 2: Escaping if shell commands are unavoidable
If you must use shell commands for a legacy workflow, always escape user input with escapeshellarg:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class ReportController extends Controller
{
public function generate(Request $request)
{
$token = $request->header('Authorization');
if (!$token) {
return response('Unauthorized', 401);
}
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS256'));
$username = $decoded->username;
if (!preg_match('/^[a-zA-Z0-9_-]+$/D', $username)) {
return response('Invalid username', 400);
}
// Proper escaping; note: better to avoid shell_exec entirely
$safeUsername = escapeshellarg($username);
$output = shell_exec("tar -czf /tmp/report-{$safeUsername}.tar.gz /var/reports 2&& echo OK");
return response($output);
}
}
</code>
Additional defenses include rotating JWT secrets, using asymmetric algorithms (RS256) to prevent secret brute-forcing, and applying principle of least privilege to the runtime environment so that any compromised command execution is limited in impact. Monitor logs for unusual token usage patterns and validate algorithm settings to prevent none algorithm attacks.
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 |