Skip to content

fix: fence untrusted log output, cap numeric inputs, pin actions to SHAs - #7

Merged
AlejandroEsquivel merged 3 commits into
mainfrom
fix/public-hardening
Sep 16, 2026
Merged

AlejandroEsquivel merged 3 commits into
mainfrom
fix/public-hardening

Conversation

@cubeorgdev

@cubeorgdev cubeorgdev Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Untrusted text is fenced before logging. The runner treats any log line starting with :: as a workflow command. The agent's final_message and the API-supplied session URL were written with core.info verbatim, so an agent reply containing ::add-mask::e or ::error::... could mask characters in later logs or spoof annotations. Both now go through a logUntrusted helper that wraps them in a ::stop-commands::<random-uuid> block.
  • Numeric inputs are capped. timeout_minutes ≤ 360 (GitHub's job limit) and poll_interval_seconds ≤ 3600. A value above 2^31-1 ms is clamped to 1 ms by Node's setTimeout, which turned the poll loop into a tight loop against the API.
  • Entry-point guard handles symlinks. Node realpath-resolves the main module but argv[1] is not resolved, so a symlinked action directory skipped main() and exited 0 with no outputs. Both forms are now compared.
  • Trimmed API key is masked too, since the client sends the trimmed form.

Workflows

  • Third-party actions pinned to commit SHAs with version comments (actions/checkout v5.1.0, actions/setup-node v5.0.0, github/codeql-action v4.38.0).
  • timeout-minutes on every job.
  • release.yml skips pre-releases and refuses to move the floating major tag to a commit that is not an ancestor of main.

Docs and examples

  • The README quick-start and the example's report step interpolated final_message / pull_requests straight into run: via ${{ }}, which SECURITY.md advises against. Both now pass outputs through env:.
  • Dropped the example's unused pull-requests: write permission. The action never uses GITHUB_TOKEN.
  • README and action.yml document the new input maxima.

Other

  • scripts/mock-api.mjs used https://api.prod.blocks.team as its fake foreign host. That is a real, resolvable hostname and showed up in every CI log. Replaced with https://api.example.invalid.
  • .gitignore ignores .env and .env.*.

Repo settings changed alongside this PR

  • Two tag rulesets are active next to "Protect main": Protect version tags (v*.*: no update, force-push, or deletion) and Protect floating major tags (v1, v2, …: no deletion; updates stay allowed because the release workflow moves them with GITHUB_TOKEN). Admins bypass both.
  • CodeQL default setup switched off. GitHub auto-enabled it and rejects results from an advanced-configuration workflow while it is on. codeql.yml is the intended configuration (excludes dist/, pinned versions, required check in the ruleset), and its first live run on this PR passed.

Testing

  • npm run lint, npm run type-check: clean. npm run test: 66 tests (4 new: input caps, logUntrusted).
  • actionlint 1.7.12 + shellcheck: clean on all three workflows.
  • Rebuilt dist/index.mjs and ran it end-to-end against the mock: stop-commands fencing visible in the log, outputs unchanged.
  • Symlinked-entrypoint run executes main() and fails on the missing agent as expected, instead of exiting 0 silently.

Follow-ups (settings, not in this PR)

  • Enable "Require actions to be pinned to a full-length commit SHA" in Settings → Actions once merged.
  • Consider requiring approval for workflow runs from all external contributors rather than first-time only.

🤖 Generated with Claude Code

…ob summary, cap inputs, pin actions

- Log agent/API text inside a stop-commands block so it cannot inject
  runner workflow commands (add-mask, error, ...).
- HTML-escape everything in the job summary and only link https URLs.
- Cap timeout_minutes (360) and poll_interval_seconds (3600) so a large
  value cannot overflow setTimeout into a tight poll loop.
- Compare realpath(argv[1]) too, so a symlinked action dir still runs main().
- Mask the trimmed API key as well as the raw input.
- README/example: pass step outputs through env, not ${{ }} in run:;
  drop the unused pull-requests: write.
- Pin third-party actions to commit SHAs; timeout-minutes on every job.
- release.yml: skip pre-releases; refuse to move the major tag to a
  commit that is not on main.
- Mock API uses a reserved .invalid host instead of a real hostname.
- Add dependabot.yml (actions weekly, npm security-only) and ignore .env.
@cubeorgdev

cubeorgdev Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

This pull request was created from a Blocks session.

View in dashboard | View on desktop

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@cubeorgdev cubeorgdev Bot changed the title fix: harden for public use — fence untrusted log output, escape the job summary, cap inputs, pin actions fix: fence untrusted log output, cap numeric inputs, pin actions to SHAs Sep 16, 2026
@AlejandroEsquivel
AlejandroEsquivel marked this pull request as ready for review September 16, 2026 08:39
@AlejandroEsquivel
AlejandroEsquivel merged commit 46a5146 into main Sep 16, 2026
6 checks passed
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