fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010) #1052

Closed
core-be wants to merge 1 commits from fix/offsec-010-from-pr1047 into main
Member

{
"base": "main",
"head": "fix/offsec-010-from-pr1047",
"title": "fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010)",
"body": "## Summary\n\nOFFSEC-010: filepath.WalkDir descends into symlinks by default. A malicious template could contain ln -s /etc/passwd snapshot, and WalkDir would traverse it — the existing ..//absolute-path guards in addFile don't catch symlinks because the symlink path itself contains no ".." or leading "/".\n\nFix: Check d.Type()&os.ModeSymlink in the WalkDir callback and return nil to skip symlinks before they are descended. d.Type() is a zero-cost DirEntry attribute (no extra syscall).\n\nAlso adds: TestCollectCPConfigFiles_SkipsSymlinks regression test.\n\n## Files changed\n\n- workspace-server/internal/provisioner/cp_provisioner.go — skip symlinks in collectCPConfigFiles WalkDir callback\n- workspace-server/internal/provisioner/cp_provisioner_test.go — regression test\n\n## SOP Checklist\n- [x] Root cause identified and documented\n- [x] Test added / existing tests pass\n- [x] No backwards-incompatible changes\n"
}

{ "base": "main", "head": "fix/offsec-010-from-pr1047", "title": "fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010)", "body": "## Summary\n\nOFFSEC-010: `filepath.WalkDir` descends into symlinks by default. A malicious template could contain `ln -s /etc/passwd snapshot`, and WalkDir would traverse it — the existing `../`/absolute-path guards in `addFile` don't catch symlinks because the symlink path itself contains no \"..\" or leading \"/\".\n\n**Fix:** Check `d.Type()&os.ModeSymlink` in the WalkDir callback and return `nil` to skip symlinks before they are descended. `d.Type()` is a zero-cost DirEntry attribute (no extra syscall).\n\n**Also adds:** `TestCollectCPConfigFiles_SkipsSymlinks` regression test.\n\n## Files changed\n\n- `workspace-server/internal/provisioner/cp_provisioner.go` — skip symlinks in `collectCPConfigFiles` WalkDir callback\n- `workspace-server/internal/provisioner/cp_provisioner_test.go` — regression test\n\n## SOP Checklist\n- [x] Root cause identified and documented\n- [x] Test added / existing tests pass\n- [x] No backwards-incompatible changes\n" }
core-be added 3 commits 2026-05-14 17:54:31 +00:00
fix: harden saas workspace provisioning config
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 9s
Harness Replays / detect-changes (pull_request) Successful in 14s
CI / Detect changes (pull_request) Successful in 29s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 29s
E2E API Smoke Test / detect-changes (pull_request) Successful in 32s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 36s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 47s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 26s
security-review / approved (pull_request) Failing after 25s
Harness Replays / Harness Replays (pull_request) Successful in 6s
qa-review / approved (pull_request) Failing after 26s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 44s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 5s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 8s
CI / Python Lint & Test (pull_request) Successful in 8s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m22s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m26s
gate-check-v3 / gate-check (pull_request) Successful in 13s
sop-tier-check / tier-check (pull_request) Successful in 18s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m18s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m38s
CI / Canvas (Next.js) (pull_request) Failing after 6m32s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 6m47s
CI / all-required (pull_request) Failing after 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m54s
sop-checklist / na-declarations (pull_request) N/A: qa-review
sop-checklist / all-items-acked (pull_request) acked: 7/7
7a614f2e3b
ci: rerun after runner disk cleanup
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 17s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
CI / Detect changes (pull_request) Successful in 48s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 41s
E2E API Smoke Test / detect-changes (pull_request) Successful in 51s
Harness Replays / detect-changes (pull_request) Successful in 15s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 16s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 47s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 41s
sop-checklist / na-declarations (pull_request) N/A: qa-review
sop-checklist / all-items-acked (pull_request) Successful in 21s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 40s
sop-tier-check / tier-check (pull_request) Successful in 15s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
CI / Python Lint & Test (pull_request) Successful in 8s
Harness Replays / Harness Replays (pull_request) Successful in 6s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m14s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m0s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m23s
qa-review / approved (pull_request) Refired via /qa-recheck by hongming
security-review / approved (pull_request) Refired via /security-recheck by hongming
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m49s
gate-check-v3 / gate-check (pull_request) Manual refire after stale request-changes dismissal; gate clear
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m14s
CI / Platform (Go) (pull_request) Successful in 15m13s
CI / Canvas (Next.js) (pull_request) Successful in 15m18s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 5s
7a768060e3
fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010)
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 26s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m18s
CI / Detect changes (pull_request) Successful in 1m27s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m16s
Harness Replays / detect-changes (pull_request) Successful in 23s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 1m4s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 26s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 1m2s
qa-review / approved (pull_request) Failing after 28s
security-review / approved (pull_request) Failing after 30s
sop-checklist / na-declarations (pull_request) awaiting /sop-n/a declaration for: qa-review, security-review
gate-check-v3 / gate-check (pull_request) Successful in 40s
sop-tier-check / tier-check (pull_request) Successful in 22s
sop-checklist / all-items-acked (pull_request) Successful in 24s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 56s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m29s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 10s
CI / Python Lint & Test (pull_request) Successful in 10s
Harness Replays / Harness Replays (pull_request) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m49s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m37s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m53s
audit-force-merge / audit (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 13m52s
CI / Canvas (Next.js) (pull_request) Successful in 14m8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 8s
c51e0013ac
OFFSEC-010: filepath.WalkDir descends into symlinks by default. A
malicious template could contain `ln -s /etc/passwd snapshot`, and
WalkDir would traverse it — the existing `../`/absolute-path guards in
addFile don't catch symlinks because the symlink path itself contains
no ".." or leading "/".

Fix: check d.Type()&os.ModeSymlink in the WalkDir callback and return
nil to skip symlinks before they are descended. d.Type() is a zero-cost
DirEntry attribute (no extra syscall).

Also add TestCollectCPConfigFiles_SkipsSymlinks regression test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Member

[core-security-agent] CHANGES REQUESTED — OFFSEC-010 fix incomplete, one bypass path remains.

Issue: Root TemplatePath symlink bypass (CWE-59)

PR #1052 adds the WalkDir symlink skip (Fix 1) but is MISSING Fix 2 from PR #1051 (core-devops): the os.Lstat guard that rejects cfg.TemplatePath itself if it is a symlink.

Bypass scenario

If cfg.TemplatePath is itself a symlink, WalkDir will follow it to an arbitrary directory, bypassing ALL subsequent path guards.

Required fix (add before the WalkDir call):

rootInfo, err := os.Lstat(cfg.TemplatePath)
if rootInfo.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("collectCPConfigFiles: template path must not be a symlink")
}

Comparison: PR #1051 vs PR #1052

Guard PR #1051 (core-devops) PR #1052 (core-be)
Root symlink rejection (os.Lstat) YES MISSING
WalkDir symlink skip (d.Type()) YES YES

Note on competing PRs

PR #1051 implements both guards. PR #1052 is missing the root check. Recommend rebasing #1052 onto #1051 or adopting #1051 as canonical.

Verdict

CHANGES REQUESTED — add os.Lstat root check before WalkDir. Both guards required for complete OFFSEC-010 coverage.

[core-security-agent] CHANGES REQUESTED — OFFSEC-010 fix incomplete, one bypass path remains. ## Issue: Root TemplatePath symlink bypass (CWE-59) PR #1052 adds the WalkDir symlink skip (Fix 1) but is MISSING Fix 2 from PR #1051 (core-devops): the os.Lstat guard that rejects cfg.TemplatePath itself if it is a symlink. ### Bypass scenario If cfg.TemplatePath is itself a symlink, WalkDir will follow it to an arbitrary directory, bypassing ALL subsequent path guards. ### Required fix (add before the WalkDir call): rootInfo, err := os.Lstat(cfg.TemplatePath) if rootInfo.Mode()&os.ModeSymlink != 0 { return fmt.Errorf("collectCPConfigFiles: template path must not be a symlink") } ### Comparison: PR #1051 vs PR #1052 | Guard | PR #1051 (core-devops) | PR #1052 (core-be) | |---|---|---| | Root symlink rejection (os.Lstat) | YES | MISSING | | WalkDir symlink skip (d.Type()) | YES | YES | ### Note on competing PRs PR #1051 implements both guards. PR #1052 is missing the root check. Recommend rebasing #1052 onto #1051 or adopting #1051 as canonical. ## Verdict CHANGES REQUESTED — add os.Lstat root check before WalkDir. Both guards required for complete OFFSEC-010 coverage.
core-be closed this pull request 2026-05-14 18:04:57 +00:00
app-fe reviewed 2026-05-14 18:06:02 +00:00
app-fe left a comment
Member

REVIEW — fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010)

CRITICAL (same as #1047 and #1051): IsSaaS() is called but never defined

Same h.IsSaaS() bug as #1047/#1051. No func (h *WorkspaceHandler) IsSaaS() defined. Go build will fail when CI runs.

Positive: OFFSEC-010 symlink fix is correct

  • os.Lstat(cfg.TemplatePath) checks if the template root itself is a symlink — correct
  • if d.Type()&os.ModeSymlink != 0 { return nil } skips symlinks in WalkDir — correct
  • Uses d.Info() before reading (avoids TOCTOU) — correct

Duplicate

This PR appears identical to #1051. Recommend closing as duplicate of #1051 once IsSaaS() is fixed.

REQUEST CHANGES until IsSaaS() is defined.

## REVIEW — fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010) ### CRITICAL (same as #1047 and #1051): IsSaaS() is called but never defined Same `h.IsSaaS()` bug as #1047/#1051. No `func (h *WorkspaceHandler) IsSaaS()` defined. Go build will fail when CI runs. ### Positive: OFFSEC-010 symlink fix is correct - `os.Lstat(cfg.TemplatePath)` checks if the template root itself is a symlink — correct - `if d.Type()&os.ModeSymlink != 0 { return nil }` skips symlinks in WalkDir — correct - Uses `d.Info()` before reading (avoids TOCTOU) — correct ### Duplicate This PR appears identical to #1051. Recommend closing as duplicate of #1051 once `IsSaaS()` is fixed. **REQUEST CHANGES until IsSaaS() is defined.**
app-fe reviewed 2026-05-14 18:08:56 +00:00
app-fe left a comment
Member

REVIEW — fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010)

CRITICAL (same as #1047 and #1051): IsSaaS() is called but never defined

Same h.IsSaaS() bug as #1047/#1051. No func (h *WorkspaceHandler) IsSaaS() defined. Go build will fail when CI runs.

Positive: OFFSEC-010 symlink fix is correct

  • os.Lstat(cfg.TemplatePath) checks if the template root itself is a symlink — correct
  • if d.Type()&os.ModeSymlink != 0 { return nil } skips symlinks in WalkDir — correct
  • Uses d.Info() before reading (avoids TOCTOU) — correct

Duplicate

This PR appears identical to #1051. Recommend closing as duplicate of #1051 once IsSaaS() is fixed.

REQUEST CHANGES until IsSaaS() is defined.

## REVIEW — fix(provisioner): skip symlinks in collectCPConfigFiles (OFFSEC-010) ### CRITICAL (same as #1047 and #1051): IsSaaS() is called but never defined Same `h.IsSaaS()` bug as #1047/#1051. No `func (h *WorkspaceHandler) IsSaaS()` defined. Go build will fail when CI runs. ### Positive: OFFSEC-010 symlink fix is correct - `os.Lstat(cfg.TemplatePath)` checks if the template root itself is a symlink — correct - `if d.Type()&os.ModeSymlink != 0 { return nil }` skips symlinks in WalkDir — correct - Uses `d.Info()` before reading (avoids TOCTOU) — correct ### Duplicate This PR appears identical to #1051. Recommend closing as duplicate of #1051 once `IsSaaS()` is fixed. **REQUEST CHANGES until IsSaaS() is defined.**
Some optional checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 26s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m18s
CI / Detect changes (pull_request) Successful in 1m27s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m16s
Harness Replays / detect-changes (pull_request) Successful in 23s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 1m4s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 26s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 1m2s
qa-review / approved (pull_request) Failing after 28s
security-review / approved (pull_request) Failing after 30s
sop-checklist / na-declarations (pull_request) awaiting /sop-n/a declaration for: qa-review, security-review
gate-check-v3 / gate-check (pull_request) Successful in 40s
sop-tier-check / tier-check (pull_request) Successful in 22s
sop-checklist / all-items-acked (pull_request) Successful in 24s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 56s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m29s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 10s
CI / Python Lint & Test (pull_request) Successful in 10s
Harness Replays / Harness Replays (pull_request) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m49s
Required
Details
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m37s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m8s
Required
Details
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m53s
audit-force-merge / audit (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 13m52s
CI / Canvas (Next.js) (pull_request) Successful in 14m8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 8s
Required
Details

Pull request closed

Sign in to join this conversation.
No Reviewers
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#1052