Skip to content

fix(controller): allowlist module names to close command injection (D2)#13

Merged
luisguzman-adfa merged 1 commit into
mainfrom
fix/phase1-security-d2-module-injection
Jun 18, 2026
Merged

fix(controller): allowlist module names to close command injection (D2)#13
luisguzman-adfa merged 1 commit into
mainfrom
fix/phase1-security-d2-module-injection

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

Summary

Phase 1 (security hardening). DeployFragment.processNextInQueue() interpolates a module name into a command executed as root inside the container:

sed -i -E '/^...NAME_(install|enabled).../d' /etc/iiab/local_vars.yml &&
echo 'NAME_install: True' >> ... && echo 'NAME_enabled: True' >> ... &&
cd /opt/iiab/iiab && ./runrole NAME

Module names come from the fixed ModuleRegistry catalog (the user only ticks checkboxes), so this is not exploitable today — but the queue round-trips through SharedPreferences and the value is concatenated raw, so a tampered/unknown name with shell metacharacters (', ;, &&, $()) would inject commands run as root. Tech-debt item D2.

Changes (allowlist, fail-closed — no behaviour change for real modules)

  • deploy/domain/ModuleName (new, pure JVM): isAllowed(name, known) = the name is a known catalog key (allowlist) and matches [a-z0-9_-] (no uppercase, whitespace, quotes or shell metacharacters). Unit-tested (ModuleNameTest).
  • ModuleRegistry.validYamlKeys(): the single allowlist source, derived from MASTER_ROSTER (the catalog stays the source of truth).
  • DeployFragment: validates the popped module before building the command and fails closed — an unrecognized/unsafe name is logged and skipped. The command string and the flow are unchanged for legitimate modules.

Design notes / what we deliberately did NOT change

We chose an exact allowlist by roster (gold standard for command injection) over a looser charset-only check or a deeper refactor of the install flow / PRootEngine (which would touch the god class and risk breaking the install). The fully-static commands (./runrole --reinstall maps, the bootstrap bash -c) are not interpolated and are left as-is. The lower-risk on-device sh -c pipes in the extract/backup paths (which interpolate app-internal absolute paths, not container-root) are a documented follow-up.

Testing

ModuleNameTest (pure JVM, runs in the CI testDebugUnitTest gate): accepts every roster module; rejects injection payloads, well-formed-but-unknown names, and bad charsets. Logic additionally verified out of band (27 cases).

DeployFragment.processNextInQueue() interpolated the module name into a
sed/echo/runrole command executed as root inside the container. Module names
come from the fixed ModuleRegistry catalog but round-trip through
SharedPreferences, so a tampered or unknown value containing shell
metacharacters (quote, ;, &&, $()) could inject commands run as root
(tech-debt D2).

- domain: new pure ModuleName.isAllowed(name, known) -- the name must be a
  known catalog key (allowlist) AND match [a-z0-9_-] with no shell
  metacharacters. Pure JVM, unit-tested (ModuleNameTest).
- ModuleRegistry.validYamlKeys(): single allowlist source derived from
  MASTER_ROSTER (the catalog stays the source of truth).
- DeployFragment guards the popped module before building the command and
  fails closed (logs + skips) on anything not allowed. The command structure is
  unchanged for legitimate modules, so behaviour for real installs is identical.

Out of scope (documented follow-up): the lower-risk on-device `sh -c` pipes in
the extract/backup paths, which interpolate app-internal absolute paths.
@luisguzman-adfa luisguzman-adfa force-pushed the fix/phase1-security-d2-module-injection branch from 2d6aaa0 to c460b5c Compare June 18, 2026 20:04
@luisguzman-adfa luisguzman-adfa merged commit 8307ec3 into main Jun 18, 2026
1 check passed
@luisguzman-adfa luisguzman-adfa deleted the fix/phase1-security-d2-module-injection branch June 23, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant