feat: handle client payload oversized parsing - #566
Conversation
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Infrastructure as Code | View in Orca | ||
| OSS Licenses | View in Orca | ||
| SAST | View in Orca | ||
| Secrets | View in Orca | ||
| Vulnerabilities | View in Orca |
There was a problem hiding this comment.
✨ PR Review
The PR correctly refactors payload resolution into a dedicated pre-step to handle plain, compressed, and reference-based payloads. The logic is well-structured, but there is a critical security issue with token handling and a notable robustness gap in the gzip decompression path.
2 issues detected:
🔒 Security - A GitHub token is emitted as an unmasked step output, making it readable in the Actions UI and log artifacts. 🛠️
Details: payload.githubToken is written to a step output via core.setOutput('github_token', ...). Step outputs in GitHub Actions are stored in plain text and are visible in the workflow run summary and logs. Any subsequent step that references steps.payload-fields.outputs.github_token will also echo the raw token value into the log if the expression is ever printed or an error occurs. The token must be masked before it is used anywhere.
File: action.yml (89-89)
🛠️ A suggested code correction is included in the review comments.
🐞 Bug - A zlib decompression error inside `inflate` propagates as an opaque exception with no useful context about what failed. 🛠️
Details: gunzipSync(buffer) in the inflate helper will throw a native zlib error if the buffer contains the correct gzip magic bytes (0x1f 0x8b) but the rest of the compressed data is malformed or truncated. This exception is not caught inside inflate, so it surfaces as an unhandled throw inside resolve, which the outer try/catch catches with a generic "Failed resolving client payload" message. The error message gives no indication that decompression failed, making debugging harder and also means a partially-corrupted payload is silently treated as a fatal error rather than giving a clear diagnostic.
File: action.yml (55-55)
🛠️ A suggested code correction is included in the review comments.
Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how
There was a problem hiding this comment.
Pull request overview
Adds oversized client payload handling for plain, compressed, and server-stashed payloads.
Changes:
- Resolves payload fields before dependent steps.
- Uses resolved outputs for checkout tokens, URLs, repositories, and refs.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
client_payload carries githubToken, and every step that takes the payload as env logs it in plaintext in its own env dump. Registering it as a secret from the first step that parses the payload masks those occurrences. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✨ PR Review
The PR correctly handles the three client_payload forms (plain, compressed, referenced) and replaces fragile inline fromJSON(fromJSON(...)) expressions with a dedicated resolution step. The core.setSecret call for the github token is a good addition. A few security and reliability issues remain.
3 issues detected:
🔒 Security - `reference.resolverToken` is used in a network request without being registered as a secret, leaving it exposed in logs. 🛠️
Details: reference.resolverToken is passed directly in the Authorization header at line 68 but is never registered with core.setSecret. If the runner echoes the authorization header (e.g., on a redirect, a curl trace, or an error that prints request details), the token will appear in plain text in the workflow logs.
File: action.yml (67-70)
🛠️ A suggested code correction is included in the review comments.
🐞 Bug - `process.exit(1)` bypasses the `actions/github-script` teardown and output-flushing machinery. 🛠️
Details: process.exit(1) at line 102 terminates the Node.js process immediately. Inside actions/github-script, this skips the action's internal teardown (output flushing, annotation writing) and may leave the step in an ambiguous state on some runner versions. The idiomatic way to signal failure from within a github-script block is core.setFailed(message), which marks the step as failed, writes the error annotation, and lets the runner finish cleanly.
File: action.yml (101-102)
🛠️ A suggested code correction is included in the review comments.
🔒 Security - An externally-controlled URL is fetched without origin validation, creating a server-side request forgery vector from the Actions runner.
Details: reference.payloadUrl is fetched at line 67 without any allowlist or origin validation. The client_payload input is supplied by the external event that triggered the workflow. A malicious dispatcher could set payloadUrl to an internal network address (e.g., instance metadata endpoint http://169.254.169.254/...) and the Actions runner would make that request, potentially leaking cloud-provider credentials or internal service responses.
File: action.yml (67-67)
Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how
…r gzip error - core.setSecret on reference.resolverToken before it goes into the Authorization header, so it is masked like the installation token - core.setFailed instead of core.error + process.exit(1), so the step fails through the runner's normal path without risking truncated log output - gunzipSync wrapped so a corrupt blob reports "gzip decompression failed: ..." rather than a bare zlib message - shortened the step comment per review Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✨ PR Review
The PR cleanly resolves all five previously-reported issues (token masking, decompression error handling, resolver-token secret registration, process.exit replacement, and the SSRF-enabling URL fetch). No new critical issues were introduced. One previously-raised SSRF concern (issue_5) remains unaddressed.
1 issues detected:
🔒 Security - Untrusted `payloadUrl` from the external event payload is fetched without origin validation, enabling SSRF.
Details: reference.payloadUrl is fetched without any allowlist or origin validation. A malicious dispatcher can set payloadUrl to an internal-network address (e.g., http://169.254.169.254/latest/meta-data/) and the Actions runner will make that request, potentially leaking cloud-provider credentials or internal service responses.
File: action.yml (69-69)
Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
action.yml:76
response.text()buffers the entire remote response, while the timeout only limits duration and does not bound bytes received. A referenced endpoint can return a very large or fast chunked response and exhaust runner memory. Read the body with an explicit byte cap (and reject oversizedContent-Lengthearly when present) before parsing or decompressing it.
const body = await response.text();
gzip is asymmetric: a ~66KB base64 input inflates to 50MB and can exhaust the runner before parsing. gunzipSync now runs with maxOutputLength, which covers both the compressed input and the body fetched for a stashed payload, and reports a clear message instead of a bare zlib error when the cap trips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✨ PR Review
The PR refactors client payload handling to support plain JSON, base64(gzip), and server-stashed (oversized) references, replacing fragile inline fromJSON(fromJSON(...)) YAML expressions with a dedicated resolution step. Security mitigations (secret masking, decompression bomb guard, fetch timeout) are solid. One notable logic flaw exists in the resolve function's early-exit path for the oversized reference check.
1 issues detected:
🐞 Bug - A substring match on raw input triggers a JSON.parse call that can throw for valid non-JSON payloads (e.g., base64-gzip blobs), bypassing the inflate path and failing the step.
Details: The raw.includes(OVERSIZED_PAYLOAD_REFERENCE) substring check at line 72 is performed before any parsing. If a legitimate payload (plain JSON or base64-gzip) happens to contain the string 'oversized-payload-reference' as part of a field value (e.g., in a commit message or branch name), the code will call parsePayload(raw) on it. If raw is a base64-gzip blob (not valid JSON), JSON.parse will throw, and the exception will propagate up through resolve to the outer catch, causing core.setFailed to be called. The inflate and plain-JSON paths are never tried, so the step fails on a valid payload.
File: action.yml (72-86)
Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how
reference.payloadUrl came straight from client_payload, so a crafted dispatch could aim the runner's fetch at any address - an SSRF primitive that matters most on self-hosted runners. The stash is served by the same host as the resolver, so the origin is now required to match inputs.resolver_url, which keeps prod, dev and self-hosted deployments working without hardcoding a domain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
✨ PR Review
The PR properly addresses the oversized-payload reference flow with origin validation, secret masking, and improved error handling. Several previous issues (SSRF, resolverToken masking, process.exit, inflate error clarity) are now resolved. Two previously reported issues remain open (issue_1, issue_7), and one new bug is introduced around invalid RESOLVER_URL_ARG handling.
1 issues detected:
🐞 Bug - `new URL('')` throws a TypeError, so a missing or empty `resolver_url` input causes every reference-type payload to fail with an unintelligible error. 🛠️
Details: When inputs.resolver_url is not supplied (or is an empty/malformed string), process.env.RESOLVER_URL_ARG || '' evaluates to '', and new URL('') throws TypeError: Invalid URL. This unhandled throw propagates up through resolve and is caught by the outer catch, producing a confusing "Failed resolving client payload: TypeError: Invalid URL" message with no indication that the resolver URL was never configured. Any payload that contains the string oversized-payload-reference (which triggers this code path) will silently fail the step with an opaque error.
File: action.yml (79-79)
🛠️ A suggested code correction is included in the review comments.
Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
action.yml:56
Buffer.from(..., 'base64')silently ignores non-base64 characters, so some valid plain JSON is misclassified as gzip. For example, a payload whose first key begins withH4sIdecodes to the gzip magic bytes after the JSON punctuation is discarded, then fails ingunzipSyncinstead of being parsed as plain JSON. Validate that the whole input is base64 before checking its decoded magic bytes.
const buffer = Buffer.from(value, 'base64');
if (buffer.length < 2 || buffer[0] !== 0x1f || buffer[1] !== 0x8b) {
action.yml:114
- This output is set after
githubTokenis registered withcore.setSecret. GitHub Actions does not allow a masked value to be set as an output, sosteps.payload-fields.outputs.github_tokencan be omitted and the checkout falls back togithub.token; that breaks private/cross-repository checkouts requiring the installation token. Keep the value masked but pass it to later steps through a subsequent-step environment variable rather than an output.
core.setOutput('github_token', githubToken);
✨ PR Description
Purpose: Implement a robust payload resolution step to handle plain, gzipped, or server-stashed oversized client payloads.
Main changes:
Resolve payload fieldsstep usinggithub-scriptto parse and inflate payloadsfromJSONexpressions withsteps.payload-fieldsoutputs for downstream stepsGenerated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Description using Guidelines Learn how