How to fix command injection in embedded firmware
Command injection (MITRE CWE-78 for the OS shell, CWE-77 for any product-specific command language) happens when firmware builds a command string from attacker-influenced input — a hostname, filename, SSID, or CLI argument — and passes it to a shell or command interpreter without neutralizing the characters that change its meaning. Both MITRE pages are read as of 2026-10-29. The fix is largely architectural: stop invoking a shell at all where a library call will do, and treat every remaining shell invocation as a parameterized call plus allowlist, not as a string to sanitize.
1. Detect
- Grep the firmware's web UI and CLI handlers (diagnostics, ping/traceroute, firmware upgrade, Wi-Fi/VPN configuration, log export) for calls into
system(),popen(),exec*(), backticks, or an equivalent shell-invoking call in your language/SDK, and trace every argument back to its source. - Flag any handler where a user-supplied string (device name, SSID, filename, email address) is concatenated into that command line rather than passed as a discrete, already-separated argument.
- Run static analysis with a rule set that maps to CWE-77/78 (most commercial and open-source SAST tools ship one), and treat a hit on firmware CLI/web-UI code as higher priority than the same hit in a build script, since the former is attacker-reachable over the network.
- Check CLI protocol parsers (not just the shell) for the CWE-77 case: a device's own management protocol or scripting language can carry the same risk if it does not separate commands from data.
2. Fix
- Remove the shell call. OWASP's primary defense is to "avoid calling OS commands directly" and use a built-in library function instead — a filesystem function for
mkdir/rm, a DNS resolver library for a ping-equivalent check, a native Wi-Fi configuration API instead of shelling out towpa_cli. MITRE's own architecture-and-design mitigation for CWE-78 says the same: use library calls "rather than external processes to recreate the desired functionality." - If a shell call cannot be removed, parameterize it. Call the exec-family function with the command and each argument as separate list elements (so the OS invokes the binary directly, with no shell parsing step in between), rather than building one interpolated string that a shell then re-parses.
- Add allowlist validation on top of parameterization, not instead of it: validate the command itself against a fixed allowlist, and validate each argument against a strict pattern (for example, a hostname regex) that rejects shell metacharacters (ampersand, pipe, semicolon, dollar sign, angle brackets, backtick, backslash, quotes and parentheses). OWASP is explicit that parameterization alone "doesn't replace argument validation" — the two layers address different attack paths.
- Treat OS-specific escaping (e.g.
escapeshellarg()) as a last resort only, when neither of the above is possible — it still allows an attacker to control which arguments reach an otherwise-fixed command. - Run with least privilege. If the firmware process that shells out does not need root, drop privileges first, so a missed case is contained rather than a full compromise.
3. Verify
- Re-run the original report's proof-of-concept input, and a short fuzz list of shell metacharacters, against the patched handler and confirm the command no longer executes unexpected behaviour.
- Add the test case to your regression suite so a future refactor cannot silently reintroduce the same concatenation pattern.
- Re-run SAST/static analysis across the firmware image and confirm the CWE-77/78 finding is gone, not just suppressed.
4. Ship an update
Build and sign a new firmware image carrying the fix, and distribute it through your normal secure-update channel (signed image, integrity check, rollback protection). See our firmware SBOM guide to confirm which shipped models and versions actually contain the vulnerable handler before you scope the release.
5. Disclose via PSIRT
Publish an advisory describing the affected products, versions and fix, following ISO/IEC 29147/30111's receive-verify-remediate-disclose shape — see our CVD process guide — and, for EU-market products, check whether the Cyber Resilience Act's 24-/72-hour reporting duty applies if the flaw was being actively exploited; see CRA 24h/72h reporting. If you do not have a standing PSIRT process yet, our PSIRT setup guide covers what a small device maker needs.
CISA's Secure by Design Pledge lists "reducing the prevalence of one or more vulnerability classes" as one of its seven measurable goals — command injection in a repeated code pattern (every diagnostic handler shelling out the same way) is a good candidate for exactly that kind of class-wide fix; see our CISA Secure by Design guide.
SoftWSP's device and firmware scanning flags command-injection-prone code paths like these directly in a firmware image, mapped to the specific CWE: see device & firmware security.
This guide summarises public MITRE CWE and OWASP guidance; it is not a substitute for a full source-code review and does not constitute legal advice.
Frequently asked questions
What is command injection?
MITRE's CWE-78 (OS Command Injection) defines it as a product that "constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command before it is sent to a downstream component." CWE-77 covers the broader case of any custom command language, not just the OS shell — MITRE notes that "developers may not realize that these other command languages might also be vulnerable to attacks," which matters for firmware CLIs and management-protocol parsers that implement their own command syntax.
Why is command injection common in device firmware specifically?
Firmware web UIs and CLI handlers frequently shell out to BusyBox or system binaries for ping, traceroute, firmware upgrade, Wi-Fi configuration or diagnostics, passing a user-supplied hostname, filename or SSID straight into a shell string. MITRE's demonstrative examples for CWE-78 show exactly this pattern — a PHP or C handler building a command line by string concatenation — which is how most router, camera and industrial-gateway OS-command-injection CVEs in this site's advisories arise.
What is the single most effective fix?
Per OWASP's OS Command Injection Defense Cheat Sheet, "the primary defense is to avoid calling OS commands directly" and instead use a built-in library function for the same task (its example: replace `system("mkdir /dir_name")` with the language's own `mkdir()` call), because a library function "cannot be manipulated to perform tasks other than those it is intended to do." This removes the shell parser — and the injection risk — entirely, rather than trying to filter dangerous input to it.
If we cannot avoid shelling out, what is the fallback?
OWASP describes two more layers, to be used together rather than as alternatives: parameterization — "structured mechanisms that automatically enforce the separation between data and command," i.e. pass arguments as a list/array to an exec-family call that does not invoke a shell, not as one interpolated string — plus allowlist validation of both the command and each argument, rejecting metacharacters such as ampersand, pipe, semicolon, dollar sign, angle brackets, backtick, backslash, quotes and parentheses. OS-specific escaping functions (for example PHP's `escapeshellarg()`) are a weaker last resort: OWASP notes they still let an attacker pass arbitrary arguments to the allowed command.
How do we verify the fix actually closes the hole?
Re-test with the same shell metacharacters the original report or your own fuzzing used (`; & | \n` and backticks) against every input that reaches the changed code path, confirm the firmware's command-injection static-analysis or SAST rule for the relevant CWE no longer fires, and keep the test case in your regression suite — CISA's Secure by Design pledge asks signatories to show a measurable reduction in a vulnerability class over time, not a single one-off fix; see our [CISA Secure by Design guide](/guides/cisa-secure-by-design-pledge/).