Skip to content

Harden workflow YAML and scanner file handling - #67473

Open
pelikhan with Copilot wants to merge 2 commits into
mainfrom
copilot/aw-top-10-resolve-injection-alerts
Open

pelikhan with Copilot wants to merge 2 commits into
mainfrom
copilot/aw-top-10-resolve-injection-alerts

Conversation

Copilot AI commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Scanner findings covered unsafe generated YAML quoting, GraphQL interpolation, file races, trace escaping, and an unpinned Docker installer.

  • YAML: Emit the maintenance token through the existing YAML scalar helper and test round-tripping quotes, comments, backslashes, and newlines.
    GH_AW_MAINTENANCE_GITHUB_TOKEN: "${{ secrets.MAINTENANCE_TOKEN }}"
  • File handling: Use exclusive file creation and read from open descriptors in the verification scripts; check ledger size on the same descriptor used to read it.
  • Trace output: Escape pipes, backslashes, and line breaks in every generated Markdown table cell.
  • Existing protections: GraphQL inputs already use variables, and scanner commands already validate executable paths and arguments. The cited Docker installer is absent following removal of the docker-sbx runtime.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix open injection and scanner alerts Harden workflow YAML and scanner file handling Oct 10, 2026
Copilot AI requested a review from pelikhan October 10, 2026 16:58
@pelikhan
pelikhan marked this pull request as ready for review October 10, 2026 17:53
Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:53
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Lean already. Ship.

Generated by Ponytail Reviewer for #67473

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

No ADR enforcement needed for PR #67473: it lacks the "implementation" label and has only 30 new lines in business logic directories (threshold: 100).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

The descriptor-based read is the right direction, but the TLC evidence bundle is still mutable after the verdict is computed.

Blocking theme
  • .github/scripts/work-queue-formal-check.cjs now classifies from the original descriptor, but it never re-materializes bundle/tlc.log from those trusted bytes. In the same path-replacement scenario covered by the new test, the uploaded log artifact can still disagree with result.json and summary.md.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 57 AIC · ⌖ 5.62 AIC · ⊞ 19.9K
Comment /review to run again

log = fs.readFileSync(logPath, "utf8");
log = readFileDescriptor(fd);
checkpoints = checkpointBundle(stateDir, bundleDir, options.checkpointMaxBytes ?? CHECKPOINT_MAX_BYTES, log.includes("Checkpointing completed"), env);
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the verdict from the original file descriptor fixes the TOCTOU on classification, but bundle/tlc.log itself can still be swapped to attacker-controlled content, so the uploaded evidence no longer matches the result you just computed.

💡 Keep the artifact bound to the descriptor you trusted

The new replace_log test demonstrates this: after tlc.log is replaced with a symlink, readFileDescriptor(fd) sees the real TLC output, but every later consumer of bundle/tlc.log reads the replacement file instead. That means result.json / summary.md can say passed while the bundled tlc.log shows unrelated content.

Please either fail if logPath no longer resolves to the inode behind fd, or rewrite logPath from the descriptor content before packaging the bundle, for example:

const log = readFileDescriptor(fd);
fs.closeSync(fd);
fs.writeFileSync(logPath, log, { flag: "w", mode: 0o600 });

That keeps the artifact and the computed verdict derived from the same bytes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Descriptor reads and writes can truncate data, while concurrent ledger growth can bypass the configured size limit.

3 open findings
What changed in this PR

Hardens scanner-related YAML generation, file handling, and Markdown trace rendering.

Changes:

  • Safely quotes maintenance-token YAML values.
  • Uses exclusive/open-descriptor file operations.
  • Escapes Markdown table cells and adds regression tests.
File Description
specs/​work-queue/​verify_native_test.py Tests oversized ledger rejection.
specs/​work-queue/​native_probe.cjs Reads ledgers through one descriptor.
specs/​work-queue/​compare-evaluation.mjs Hardens evidence file handling.
specs/​eslint-factory/​trace.test.mjs Tests Markdown escaping.
specs/​eslint-factory/​trace.mjs Escapes every table cell.
pkg/​workflow/​maintenance_workflow_yaml_jobs.go Quotes the maintenance token safely.
pkg/​workflow/​maintenance_workflow_triggers_test.go Updates expected quoted YAML.
pkg/​workflow/​maintenance_workflow_generation_fixes_test.go Tests special-character round-tripping.
.github/​scripts/​work-queue-formal-check.test.cjs Tests path-replacement resistance.
.github/​scripts/​work-queue-formal-check.cjs Uses exclusive files and descriptors.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +68 to +69
if (fs.fstatSync(fd).size > 80 * 1024 * 1024) throw new Error("resource_limit: adapter input exceeds 80 MiB");
data = fs.readFileSync(fd, "utf8");
Comment on lines +18 to +24
function readFileDescriptor(fd) {
const { size } = fs.fstatSync(fd);
if (size === 0) return "";
const buffer = Buffer.alloc(size);
const bytesRead = fs.readSync(fd, buffer, 0, size, 0);
return buffer.subarray(0, bytesRead).toString("utf8");
}
Comment on lines +19 to +25
const readFileDescriptor = fd => {
const { size } = fs.fstatSync(fd);
if (size === 0) return "";
const buffer = Buffer.alloc(size);
const bytesRead = fs.readSync(fd, buffer, 0, size, 0);
return buffer.subarray(0, bytesRead).toString("utf8");
};
@github-actions github-actions Bot mentioned this pull request Oct 10, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /codebase-design (triage via pr-triage agent; classified bug_fix). This is a well-scoped hardening PR — each fix is paired with a regression test that reproduces the original scanner finding (YAML injection round-trip, symlink-swap log read, oversized-ledger-via-fd, Markdown cell escaping).

📋 Key Themes & Highlights

Key Themes

  • Duplicated TOCTOU helper: readFileDescriptor in specs/work-queue/compare-evaluation.mjs is a byte-for-byte copy of the one added to .github/scripts/work-queue-formal-check.cjs. Exporting and reusing it would avoid future drift in a security-relevant code path (flagged inline).
  • One inline comment I posted about missing test coverage for the resultFd catch-path was based on a mistaken premise — I verified afterward (by running the script directly) that the existing "CLI checksum rejection retains actual and pinned digests..." test does exercise that exact path, since result.json is written via writeJSON before the checksum throw. No action needed there; please disregard that specific sub-claim if raised, I'm noting the correction here for transparency.

Positive Highlights

  • ✅ writeYAMLEnv reuse for the maintenance token is the right fix — consistent with the existing scalar-escaping helper, and TestMaintenanceWorkflowQuotesCompileGitHubToken round-trips the generated YAML through yaml.v3.Unmarshal to prove the token survives intact, a strong regression test.
  • ✅ native_probe.cjs and work-queue-formal-check.cjs now check fstatSync(fd).size/read from the already-open descriptor instead of re-opening the path — correctly closes the TOCTOU window the scanner flagged, and verify_native_test.py/replace_log test cases exercise it.
  • ✅ readableTrace's escapeCell correctly handles backslash-before-pipe ordering (escape \ first, then |) and collapses all three newline variants to <br>, with a direct unit test (trace.test.mjs) asserting exact escaped output.

No blocking issues — approving with the one non-blocking suggestion (helper dedup) left as an inline comment.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 142.7 AIC · ⌖ 14.7 AIC · ⊞ 10.3K
Comment /matt to run again

const jar = process.env.TLA2TOOLS_JAR;
assert(jar && path.isAbsolute(jar), "TLA2TOOLS_JAR must be an absolute pinned jar path");
const hash = value => crypto.createHash("sha256").update(value).digest("hex");
const readFileDescriptor = fd => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] readFileDescriptor is byte-for-byte identical to the one just added in .github/scripts/work-queue-formal-check.cjs. Export it from that module (it already exports several helpers) and import it here instead of duplicating the implementation.

💡 Suggested fix
// work-queue-formal-check.cjs
module.exports = { TLC_SHA256, classify, inventory, checkpointBundle, runVerification, readFileDescriptor };

// compare-evaluation.mjs
import { TLC_SHA256, classify, readFileDescriptor } from "../../.github/scripts/work-queue-formal-check.cjs";

Two copies of a security-relevant TOCTOU-avoidance helper are easy to let drift out of sync during future edits.

@copilot please address this.

} catch (openError) {
if (!openError || openError.code !== "ENOENT") throw openError;
}
if (resultFd !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] This fd-based rewrite of the tool-error bundle is a good TOCTOU fix, but it's untested — the only exercised catch path (CLI checksum rejection retains actual and pinned digests..., line ~215 in the test file) fails before bundle/result.json exists, so it never opens the fd here. A regression test that lets writeJSON(resultPath, base) succeed, then forces runVerification to reject afterward (e.g. by making the spawned javaBin fixture exit with an unexpected signal) would confirm the read/rewrite path and guard the fd-leak-on-ENOENT edge against regressions.

💡 Why this matters

The prior version used fs.existsSync + fs.readFileSync/writeJSON (path-based, racy but simple). The new version opens with r+, swallows ENOENT, and closes in a finally — correct, but there's no test proving the merged JSON (status: "tool_error", preserved fields) is still written correctly through the descriptor, or that the fd is closed even when JSON.parse throws on corrupt content.

@copilot please address this.

This branch has not been deployed

No deployments
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.

[AW Top 10] 07 Resolve open injection and scanner alerts

3 participants