Skip to content

fix(safe-outputs): restrict upload_artifact to canonical allowed roots, reject sensitive paths - #50779

Merged
pelikhan merged 6 commits into
mainfrom
copilot/restrict-artifact-uploads
Aug 6, 2026
Merged

pelikhan merged 6 commits into
mainfrom
copilot/restrict-artifact-uploads

Conversation

Copilot AI commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

The upload_artifact handler accepted any absolute path, allowing agents to stage and upload arbitrary host files (/etc/passwd, $HOME/.ssh/id_rsa, .git/config, etc.) with no root restriction beyond symlink rejection.

Changes

Path validation (safe_outputs_handlers.cjs, upload_artifact.cjs)

  • Allowed-root enforcement: absolute paths must canonicalize to within GITHUB_WORKSPACE, RUNNER_TEMP, or the staging directory; everything else is rejected with an actionable error
  • Sensitive-path rejection via validateUploadSourcePath / validateSourcePath:
    • .git directory components
    • System directories (/etc, /proc, /sys, /dev, /run, /boot, /lib*, /usr/lib*)
    • HOME credential subtrees (.ssh, .aws, .gnupg, .docker, .kube, .netrc, .npmrc, .gitconfig, etc.)
  • Strict canonicalization: fs.realpathSync resolves symlink chains before any check; failure throws rather than falling back to path.resolve

Restrictive staging permissions

fs.chmodSync(destPath, 0o600) applied after every file copy in copySingleFileToStaging and copyDirectoryRecursive.

Temp directory cleanup (artifact_client.cjs)

Upload (gh-aw-artifact-upload-*) and download (gh-aw-artifact-download-*) temp directories are now removed in try/finally blocks, covering both success and failure paths.

Tests

New path validation (security) describe blocks in both test files cover: /etc rejection, .git-component rejection, out-of-bounds root rejection, allowed-workspace path acceptance, and 0o600 permission verification. Existing auto-copy tests updated to set GITHUB_WORKSPACE to match the new policy.


Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 22.6 AIC · ⊞ 5.9K · ◷
Comment /souschef to run again

Copilot AI and others added 2 commits August 6, 2026 05:48
…ensitive paths

- Add validateUploadSourcePath/validateSourcePath helpers that reject paths
  under /etc, system dirs, .git directory components, and HOME credential
  locations (.ssh, .aws, .gnupg, .docker, etc.)
- Add validateAllowedRoot to restrict absolute paths to staging dir,
  GITHUB_WORKSPACE, and RUNNER_TEMP only
- Canonicalize paths with fs.realpathSync before policy checks to prevent
  traversal escapes and symlink chains
- Apply restrictive permissions (0o600) on all staged files in both
  safe_outputs_handlers.cjs and upload_artifact.cjs copySingleFileToStaging
- Clean up upload and download temp directories in artifact_client.cjs
  using try/finally blocks in success and failure paths
- Add security tests covering /etc rejection, .git component rejection,
  out-of-bounds root rejection, allowed workspace paths, and 0o600 perms
- Update auto-copy tests to set GITHUB_WORKSPACE so they match the new
  allowed-roots policy

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…alpathSync error

- Use canonicalFilePath (not filePath) in the allowed-root error message so the
  resolved path is always shown, preventing misleading messages on traversals
- Replace silent path.resolve fallbacks with explicit errors when fs.realpathSync
  fails, eliminating any risk of a non-canonical path bypassing validation

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Restrict safe-output artifact uploads to explicit canonical roots fix(safe-outputs): restrict upload_artifact to canonical allowed roots, reject sensitive paths Aug 6, 2026
Copilot AI requested a review from pelikhan August 6, 2026 05:51
@pelikhan
pelikhan marked this pull request as ready for review August 6, 2026 05:51
Copilot AI balanced review requested due to automatic review settings August 6, 2026 05:51
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Thanks for working on artifact upload security! 🔐 This PR is properly scoped around securing artifact uploads to canonical roots, which aligns well with the security focus of #50770.

Since this is still in draft with pending implementation tasks, here's what will get this across the finish line:

  • Implement the security checks — Add the path validation helpers to safe_outputs_handlers.cjs and upload_artifact.cjs as outlined in the checklist. This includes validating against GITHUB_WORKSPACE, staging directories, and rejecting sensitive paths (.git, HOME, /etc).
  • Add unit tests — Once implementation is complete, add comprehensive tests in upload_artifact.test.cjs covering:
    • Allowed paths (GITHUB_WORKSPACE, staging dir)
    • Denied paths (HOME, /etc, .git/config)
    • Cleanup behavior on success and failure
  • Complete the checklist — Run make agent-report-progress and confirm all items are done before marking ready for review.

Looks like you have a solid plan in place. Keep pushing forward! 🚀

Generated by ✅ Contribution Check · auto · 53.8 AIC · ⊞ 8.7K · ◷

@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (default_business_additions=0).

@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

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

Sensitive nested paths and runner temporary files remain uploadable, while cleanup and canonical-root handling are incomplete.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Restricts artifact uploads to approved roots, blocks sensitive paths, hardens staging permissions, and cleans temporary artifact directories.

Changes:

  • Adds canonical path and sensitive-location validation.
  • Applies 0600 permissions to staged files.
  • Adds temporary-directory cleanup and security tests.
File summaries
File Description
actions/setup/js/upload_artifact.test.cjs Tests path policy and permissions.
actions/setup/js/upload_artifact.cjs Validates and stages upload sources.
actions/setup/js/safe_outputs_handlers.test.cjs Tests handler-side security checks.
actions/setup/js/safe_outputs_handlers.cjs Enforces source restrictions during staging.
actions/setup/js/artifact_client.cjs Cleans upload/download temporary directories.
Review details

Suppressed comments (4)

actions/setup/js/safe_outputs_handlers.cjs:2230

  • Directory requests validate only the requested directory itself. copyDirectoryRecursive then copies nested entries without calling this validator, so uploading the workspace root will stage .git/config even though a direct request for that file is rejected; nested symlinks are also silently skipped instead of rejecting the request as required. Validate every recursive entry and fail the whole request on a denied path or symlink.
      // Reject sensitive paths (system dirs, .git, HOME credentials).
      const sensitiveError = validateUploadSourcePath(canonicalFilePath);

actions/setup/js/safe_outputs_handlers.cjs:2246

  • Allowing the entire RUNNER_TEMP tree preserves a sensitive-file exfiltration path: it contains runner command files such as GITHUB_ENV/GITHUB_OUTPUT and may contain action-created credentials. The linked issue limits defaults to the staging directory and configured workspace paths; allow only the staging subdirectory under RUNNER_TEMP, not the whole temp root.
      if (process.env.RUNNER_TEMP) {
        allowedRoots.push(path.resolve(process.env.RUNNER_TEMP));
      }

actions/setup/js/upload_artifact.cjs:176

  • Adding all of RUNNER_TEMP as an allowed root permits uploads of runner command files (GITHUB_ENV, GITHUB_OUTPUT) and action-created temporary credentials, which defeats the explicit-root restriction. Keep the staging directory allowed, but do not authorize unrelated files merely because they share its temp parent.
  if (process.env.RUNNER_TEMP) {
    allowedRoots.push(path.resolve(process.env.RUNNER_TEMP));
  }

actions/setup/js/safe_outputs_handlers.cjs:2245

  • path.resolve is lexical, while canonicalFilePath comes from realpathSync. If an allowed workspace or temp root is itself a symlink, an ordinary file canonicalizes under the target path and is incorrectly rejected because it cannot match the unresolved root. Canonicalize existing allowed roots with realpathSync before comparing them.
      const allowedRoots = [path.resolve(stagingDir)];
      if (process.env.GITHUB_WORKSPACE) {
        allowedRoots.push(path.resolve(process.env.GITHUB_WORKSPACE));
      }
      if (process.env.RUNNER_TEMP) {
        allowedRoots.push(path.resolve(process.env.RUNNER_TEMP));
  • Files reviewed: 5/5 changed files
  • Comments generated: 5
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment on lines +311 to +315
const sensitiveErr = validateSourcePath(canonical);
if (sensitiveErr) {
return { copied: false, relPath: "", error: sensitiveErr };
}
const rootErr = validateAllowedRoot(canonical);
Comment on lines +2218 to +2222
// Canonicalize to detect traversal escapes and symlink chains.
let canonicalFilePath;
try {
canonicalFilePath = fs.realpathSync(filePath);
} catch (err) {
Comment thread actions/setup/js/upload_artifact.cjs Outdated
Comment on lines +169 to +175
function validateAllowedRoot(canonicalPath) {
const allowedRoots = [path.resolve(STAGING_DIR)];
if (process.env.GITHUB_WORKSPACE) {
allowedRoots.push(path.resolve(process.env.GITHUB_WORKSPACE));
}
if (process.env.RUNNER_TEMP) {
allowedRoots.push(path.resolve(process.env.RUNNER_TEMP));
Comment on lines +419 to 421
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-upload-"));
uploadPath = path.join(tmpDir, `${artifactName || "artifact"}.zip`);
createZipFromFiles(files, rootDirectory, uploadPath);
Comment on lines 240 to +242
try {
fs.copyFileSync(sourcePath, destPath);
fs.chmodSync(destPath, 0o600);

@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.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Review: fix(safe-outputs): restrict upload_artifact to canonical allowed roots, reject sensitive paths

This is a solid security improvement — mandatory root enforcement and sensitive-path rejection close real exfiltration vectors. The temp-directory cleanup (try/finally) and 0o600 staging permissions are good hygiene. A few issues worth noting:

Blocking

RUNNER_TEMP as an allowed root undermines the boundary

On GitHub-hosted runners RUNNER_TEMP resolves to /home/runner/work/_temp — but any path an agent writes to $RUNNER_TEMP can now be uploaded without restriction. The sensitive-path check only blocks a known allow-list of names; it won't catch arbitrary agent-created temp files that happen to contain sensitive data. Allowing the entire RUNNER_TEMP tree as a source root largely negates the workspace boundary.

Consider restricting source roots to the explicit staging dir + GITHUB_WORKSPACE only, and requiring that any temp-file upload be explicitly staged first.

validateSourcePath / validateUploadSourcePath are identical and duplicated

The two functions are byte-for-byte the same across safe_outputs_handlers.cjs and upload_artifact.cjs. Additions to one won't automatically apply to the other, creating a maintenance/divergence risk. Extract into safe_output_helpers.cjs (already used for isStagedMode).

Non-blocking

Directory upload only validates the top-level path — when a directory passes root/sensitive-path checks, its individual files are not re-validated. A sensitive file placed inside an allowed-root directory won't be caught. A comment documenting this assumption would help.

/tmp fallback for stagingDir — the process.env.RUNNER_TEMP || "/tmp" fallback on line 239 of safe_outputs_handlers.cjs means the staging dir can fall under /tmp in non-runner environments, overlapping with the system temp tree in an unexpected way.> 🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 40.4 AIC · ⊞ 5.3K

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🧪 Test Quality Sentinel Report

✅ Test Quality Score: 97/100 — Excellent

Analyzed 9 test(s): 9 design, 0 implementation, 0 violation(s).

📊 Metrics (9 tests)
Metric Value
Analyzed 9 (Go: 0, JS: 9)
✅ Design 9 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 8 (89%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
should reject absolute path outside GITHUB_WORKSPACE and RUNNER_TEMP safe_outputs_handlers.test.cjs design_test / behavioral_contract / high_value None
should reject path containing .git directory component safe_outputs_handlers.test.cjs design_test / behavioral_contract / high_value None
should reject absolute path under system directories like /etc safe_outputs_handlers.test.cjs design_test / behavioral_contract / high_value None
should set restrictive permissions (0o600) on staged files safe_outputs_handlers.test.cjs design_test / behavioral_contract / high_value None
rejects an absolute path under /etc upload_artifact.test.cjs design_test / behavioral_contract / high_value None
rejects a path containing a .git directory component upload_artifact.test.cjs design_test / behavioral_contract / high_value None
rejects an absolute path outside allowed roots upload_artifact.test.cjs design_test / behavioral_contract / high_value None
allows a path within GITHUB_WORKSPACE upload_artifact.test.cjs design_test / behavioral_contract / high_value None
sets restrictive permissions (0o600) on auto-copied staged files upload_artifact.test.cjs design_test / behavioral_contract / high_value None

Verdict

✅ Passed. 0% implementation tests (threshold: 30%). All 9 tests enforce security-contract invariants: path boundary rules, .git component rejection, system-directory blocking, and file-permission hardening. Test-to-production line ratios are well below the 2:1 inflation threshold (safe_outputs: 0.75×, upload_artifact: 1.03×). No violations detected.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 87.1 AIC · ⊞ 7.7K · ◷
Comment /review to run again

@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.

✅ Test Quality Sentinel: 97/100. 0% implementation tests (threshold: 30%).

@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.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on a few correctness and maintainability issues.

📋 Key Themes & Highlights

Key Themes

  1. os.homedir() not symlink-resolved — path.resolve(homeDir) only normalises the string; realpathSync must be used to match against the already-resolved canonicalPath. Silent bypass on macOS runners.
  2. Duplicated security logic — validateSourcePath / validateUploadSourcePath are copy-pasted across two files. Future changes to the deny list must be made in two places; divergence is a bypass risk.
  3. No per-file validation inside copyDirectoryRecursive — top-level directory validation is correct but individual files inside are not re-canonicalized before copying.
  4. Incomplete env-var cleanup in one test — RUNNER_TEMP is deleted but not restored after the out-of-bounds test, which can cause downstream test pollution.

Positive Highlights

  • ✅ Excellent defense-in-depth: allowed-root enforcement and sensitive-path deny list
  • ✅ realpathSync used correctly at the entry point before any check — prevents symlink traversal
  • ✅ try/finally temp dir cleanup in artifact_client.cjs is solid
  • ✅ Comprehensive security test suite covering all major rejection categories
  • ✅ 0o600 staging permissions are a good hygiene addition
  • ✅ Clear, actionable error messages for each rejection category
> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31075407042)* · sonnet46 · 57.3 AIC · ⊞ 7.1K > Comment /matt to run again

}
}

const homeDir = os.homedir();

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] path.resolve(homeDir) normalises the string but does not follow symlinks — on macOS os.homedir() often returns a symlink path (e.g. /Users/runner→/private/Users/runner), so the .ssh prefix check may never match a realpathSync-resolved canonical path.

💡 Suggested fix

Resolve the home directory with realpathSync once (outside the loop), matching exactly how canonicalPath was produced:

let resolvedHome = homeDir;
try { resolvedHome = fs.realpathSync(homeDir); } catch {}
for (const name of sensitiveNames) {
  const sensitive = path.join(resolvedHome, name);
  ...
}

Apply the same fix to the mirror function in safe_outputs_handlers.cjs.

@copilot please address this.

* Validate that a canonical absolute path does not refer to sensitive system or credential
* locations. Mirrors the check in safe_outputs_handlers.cjs for defense-in-depth.
*
* @param {string} canonicalPath - Resolved absolute path

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] The validateSourcePath function is a near-identical duplicate of validateUploadSourcePath in safe_outputs_handlers.cjs. Divergence between the two copies (e.g. one gets a new sensitive name, the other does not) is a likely source of future bypass.

💡 Suggested fix

Extract the shared validation logic into a common helper module (e.g. path_security_helpers.cjs) and require it from both files. This is a shallow interface / deep implementation — the module can expose:

// path_security_helpers.cjs
module.exports = { validateSensitivePath, validateAllowedRoot };

Both callers keep their own error-formatting layer but delegate the security logic to the shared module.

@copilot please address this.

@@ -241,6 +300,23 @@ function autoCopyToStaging(reqPath) {
if (stat === null) {

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.

[/tdd] The autoCopyToStaging path that handles a directory (not a file) is validated for root/sensitive paths, but the copyDirectoryRecursive helper does not re-validate individual files inside the directory. A hardlink or bind-mount inside an otherwise-allowed directory could bypass the per-entry symlink check if the dirent reports isFile() rather than isSymbolicLink().

💡 Suggested test + fix

Add a test that places a file outside allowed roots inside an otherwise-valid directory via a hardlink, and confirm it is rejected. For defense-in-depth, copyDirectoryRecursive should call validateSensitivePath(fs.realpathSync(srcPath)) before each copyFileSync (the canonical path of each entry might escape the parent directory on some overlay filesystems).

@copilot please address this.

const sensitive = path.join(path.resolve(homeDir), name);
if (canonicalPath === sensitive || canonicalPath.startsWith(sensitive + path.sep)) {
return `path refers to a sensitive HOME location: ${canonicalPath}`;
}

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] The system-denied list (/lib, /lib64, /usr/lib, /usr/local/lib) uses string prefix matching after path.resolve, but /usr/local/lib is a strict prefix of /usr/local/lib64 on some Linux distributions. The check canonicalPath.startsWith("/usr/local/lib" + sep) correctly handles this, but the static list approach will silently miss any new system dirs (e.g. /snap, /nix, /opt) that hold sensitive credentials on non-standard runner images.

💡 Suggested improvement

Consider adding /snap, /nix/store, and /opt to the deny list, and add a brief comment explaining that the list is conservative-but-not-exhaustive, with the allowed-root check as the primary defence. The deny list is secondary hardening, not the sole gatekeeper.

@copilot please address this.


expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("system directory"));
expect(mockArtifactClient.uploadArtifact).not.toHaveBeenCalled();
});

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.

[/tdd] The test "rejects an absolute path outside allowed roots" unconditionally calls delete process.env.RUNNER_TEMP but does not restore it in finally. If RUNNER_TEMP was set by the test environment, subsequent tests in the same process run without it, which could cause unrelated failures.

💡 Suggested fix
const savedRunnerTemp = process.env.RUNNER_TEMP;
delete process.env.RUNNER_TEMP;
try {
  // ... assertions
} finally {
  if (savedRunnerTemp !== undefined) process.env.RUNNER_TEMP = savedRunnerTemp;
}

The same pattern is correctly applied for GITHUB_WORKSPACE in the outer beforeEach/afterEach — apply it consistently for RUNNER_TEMP too.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@copilot Please run the pr-finisher skill and address the active review feedback before maintainer review:

  • Restrict allowed upload roots so the whole RUNNER_TEMP tree is not uploadable; keep uploads limited to explicit safe staging/workspace roots.
  • Revalidate recursive directory uploads so nested denied paths like .git are rejected instead of copied.
  • Deduplicate the path-validation logic and make sure allowed-root comparisons use canonicalized paths.
  • Re-run the targeted JS tests and summarize the fixes in your reply.

Failed check: agent — https://github.com/github/gh-aw/actions/runs/31075406960/job/92532429104
Run: https://github.com/github/gh-aw/actions/runs/31076105335

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 22.6 AIC · ⊞ 5.9K · ◷
Comment /souschef to run again

Copilot AI and others added 2 commits August 6, 2026 06:20
… recursive copy

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
…puts_handlers

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

[!WARNING]
Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

...

Addressed all feedback in commits fix: restrict upload roots... and refactor: extract canonicalizeAllowedRoot...:

  • Restricted allowed roots: RUNNER_TEMP is no longer an allowed upload root. Only STAGING_DIR (the specific gh-aw/safeoutputs/upload-artifacts subdirectory) and GITHUB_WORKSPACE are permitted.
  • Canonicalized root comparisons: Both validateAllowedRoot (upload_artifact.cjs) and the inline root check (safe_outputs_handlers.cjs) now call realpathSync on each allowed root before the containment check, so a symlinked GITHUB_WORKSPACE no longer causes its own files to be rejected.
  • Per-entry validation in recursive copies: copyDirectoryToStaging and copyDirectoryRecursive now canonicalize and validate every directory and file entry against validateSourcePath/validateUploadSourcePath before copying — nested paths like foo/.git/config are now rejected.
  • Deduplicated canonicalizeAllowedRoot: Extracted to shared scope in safe_outputs_handlers.cjs, consistent with the same helper in upload_artifact.cjs.
  • Tests: Updated the "outside allowed roots" tests to remove the RUNNER_TEMP deletion (it's no longer an allowed root), and added a new test verifying that a nested .git directory inside a recursively-copied directory is rejected.

All 293 targeted JS tests pass (58 upload_artifact + 235 safe_outputs_handlers).

Copilot AI requested a review from gh-aw-bot August 6, 2026 06:23
@pelikhan
pelikhan merged commit a9cdbee into main Aug 6, 2026
9 checks passed
@pelikhan
pelikhan deleted the copilot/restrict-artifact-uploads branch August 6, 2026 06:33
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restrict safe-output artifact uploads to explicit canonical roots

4 participants