Skip to content

fix(clearfolio): require canonical artifact-origin allowlist entries - #538

Draft
cursor[bot] wants to merge 23 commits into
fix/clearfolio-provider-response-boundaryfrom
cursor/bc-d8443854-d031-4b84-b085-56f439fcbf05-df45
Draft

fix(clearfolio): require canonical artifact-origin allowlist entries#538
cursor[bot] wants to merge 23 commits into
fix/clearfolio-provider-response-boundaryfrom
cursor/bc-d8443854-d031-4b84-b085-56f439fcbf05-df45

Conversation

@cursor

@cursor cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Buyer and security impact

Clearfolio artifact links must never turn arbitrary cross-origin HTTPS responses into trusted ScopeWeave redirects. This bounded child keeps the fail-closed artifact-origin policy and makes its operator contract explicit: allowlist entries must be canonical URL.origin values (or that origin plus /), protocol-relative and userinfo-as-host lookalikes stay rejected, and CLEARFOLIO_ARTIFACT_ORIGINS is documented where operators configure the service.

This supersedes the already-closed #504/#535 origin-policy vehicles. Do not land those in parallel. This PR remains a bounded follow-up to #489 and does not close #489.

Exact current stack

  • live parent: fix(clearfolio): bound provider transport and JSON responses #498 fix/clearfolio-provider-response-boundary@c7d0dc75e059d5ca4e9848750b67e2905945331d;
  • exact current child head: bbc9de2d5e39fcf94735325812017e4a25014c1c;
  • protected develop observed for this reconciliation: df0fa17bd5035af6455c889022c540b4f439e3d6;
  • Draft: yes;
  • mechanically mergeable: yes after reconciliation.

The prior child head 3d98ed25f40335eeb28c834eed6c868651b48a44 was still rooted at the stale #498 snapshot 5fa795210b0060f3034466c03f99fb013e838d13, leaving it three parent commits behind and mechanically unmergeable. Commit bbc9de2d... reconstructs the child from the exact current #498 tree, overlays only the bounded artifact-origin semantic paths, and deliberately merges the two shared paths (CHANGELOG.md, package.json) so both the parent contextual-orchestrator attribution changes and child Clearfolio coverage/contract remain present.

Fresh parent→child comparison is ahead, zero behind, has exact live parent c7d0dc75... as merge base, and contains exactly these 12 intended files:

  • ARCHITECTURE.md;
  • CHANGELOG.md;
  • README.md;
  • docs/api.md;
  • docs/deploy.md;
  • docs/doctoring/clearfolio-artifact-origin-trust.md;
  • docs/doctoring/clearfolio-production-configuration.md;
  • docs/doctoring/clearfolio-provider-response-boundary.md;
  • package.json;
  • server/clearfolio.mjs;
  • tests/unit/clearfolio-artifact-origin.test.mjs;
  • tests/unit/clearfolio-status-signal.test.mjs.

No parent-only orchestrator source/tests/docs were deleted by the reconstruction.

Contract

  • CLEARFOLIO_ARTIFACT_ORIGINS entries must equal the parsed origin or that origin plus /, so noncanonical entries such as explicit default ports, empty fragments/userinfo, path normalization tricks, and empty elements fail closed before provider transport;
  • default-deny regressions include protocol-relative, backslash-normalized, and userinfo-as-host lookalikes;
  • empty and whitespace-only allowlist values fail closed before transport;
  • deploy/API/README/architecture/doctoring/coverage registration and focused regressions describe and enforce the same authority;
  • parent fix(clearfolio): bound provider transport and JSON responses #498's redirect/resource boundary remains intact: credentials and fragments are rejected, only configured exact artifact origins may be returned, a provider-origin artifactToken may be translated to the local viewer, and an approved cross-origin token remains bound to its issuing URL.

Still out of scope

Destination DNS/IP authorization for private, loopback, and metadata address families plus constraining redirects to an accepted signed-artifact URL shape remain later #489 slices. Do not widen this child into those boundaries.

Exact-head evidence

Every workflow/check/review result predating bbc9de2d5e39fcf94735325812017e4a25014c1c is historical and non-authorizing. Fresh exact-head repository workflows were queued immediately after this reconciliation: Dependency Review 32317490096, Server Tests 32317490198, and OSV Scanner 32317490562. Queued/pending/skipped-required/cancelled/absent/neutral/failed/stale/predecessor/status-only/author-only/model-only evidence is non-passing. The only currently enumerated formal review is a predecessor-head Cursor COMMENTED review and is not current independent approval.

Merge gate

Remain Draft and do not integrate before #498/#493. After the prerequisite stack reaches protected develop, reconcile or retarget this exact bounded semantic diff onto the then-live protected head and require all then-applicable exact-head CI/browser/owned coverage/docstring, SAST/security/dependency/supply-chain/package/provenance/review gates, zero valid unresolved findings, and qualifying independent approval under live governance. No predecessor-head evidence transfers.

Refs #489

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Landing vehicle for the #504 origin-policy slice

Head 0c193aa28e730e5c1a18e7d740637f34efbbd95d on fix/clearfolio-provider-response-boundary.

This PR keeps #504's fail-closed allowlist and adds the residuals that blocked treating #504 as complete:

  • allowlist entries must equal URL.origin or that origin plus /;
  • protocol-relative, backslash-normalized, and userinfo-as-host redirects are default-deny regressions;
  • empty and whitespace CLEARFOLIO_ARTIFACT_ORIGINS fail before provider transport;
  • deploy/API/README and parent doctoring records now describe the control.

Prefer this over #504 and #535. Do not land those in parallel. Local Clearfolio unit files passed on this head. Exact-head repository and organization checks were still pending at this comment; pending evidence is non-passing.

Do not fold destination DNS/IP authorization or 302 path-shape limits into this PR. That is the next #489 slice.

I cannot approve or merge this automation-authored PR. Independent current-head approval is required after checks go green.

View PR

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

seonghobae added a commit that referenced this pull request Aug 20, 2026
…igin parent

Advance the Clearfolio capability-readiness child onto the exact current #538 parent without changing its seven-file semantic delta.
@opencode-agent opencode-agent Bot added priority: medium Normal-priority or P2 work status: draft Draft pull request type: bug Defect or incorrect behavior labels Aug 22, 2026
@seonghobae
seonghobae marked this pull request as ready for review August 24, 2026 01:31

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread server/clearfolio.mjs Outdated
Comment on lines 567 to 573
if (
url.origin !== clearfolioUrl.origin
|| url.username
|| url.password
|| url.hash
Boolean(url.username + url.password)
|| Boolean(url.hash)
|| !trustedArtifactOrigins.has(url.origin)
) {
throw new Error('clearfolio artifact-link response invalid');
}

@devin-ai-integration devin-ai-integration Bot Aug 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Default-case guard stays equivalent after refactor

With no CLEARFOLIO_ARTIFACT_ORIGINS, trustedArtifactOrigins holds only the provider origin, so !has(url.origin) matches the old url.origin !== clearfolioUrl.origin. Boolean(url.username + url.password) is truthy exactly when either credential is non-empty, matching the old url.username || url.password. The refactor at clearfolio.mjs preserves default behavior and only adds allowlist trust.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread server/clearfolio.mjs Outdated
Comment on lines 575 to 578
const token = url.searchParams.get('artifactToken');
if (token) {
if (token && url.origin === clearfolioUrl.origin) {
return `${configuration.baseUrl}/viewer/${encodeURIComponent(canonicalJobId)}?artifactToken=${encodeURIComponent(token)}`;
}

@devin-ai-integration devin-ai-integration Bot Aug 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Token rewrite scoped to provider origin

The viewer rewrite at clearfolio.mjs fires only when url.origin === clearfolioUrl.origin, so an approved cross-origin token stays on its own URL instead of being transplanted into the trusted viewer route.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread server/clearfolio.mjs
Comment on lines +324 to +328
const abortFromCaller = controller.abort.bind(controller);
if (callerSignal !== undefined) {
if (callerSignal.aborted) controller.abort(callerSignal.reason);
else callerSignal.addEventListener('abort', abortFromCaller, { once: true });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Caller abort reason not forwarded on async cancel

When the caller signal aborts asynchronously, abortFromCaller runs as an event handler, so controller.abort(event) records the DOM Event as the reason instead of callerSignal.reason that AbortSignal.any previously preserved. No caller reads it — all downstream catches collapse to fixed sanitized messages — so behavior is unaffected. The synchronous already-aborted branch still forwards the real reason.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread server/clearfolio.mjs
Comment on lines +137 to +140
function clearfolioArtifactOrigins(baseUrl) {
const trustedOrigins = new Set([new URL(baseUrl).origin]);
const configuredOrigins = process.env.CLEARFOLIO_ARTIFACT_ORIGINS;
if (configuredOrigins === undefined) return trustedOrigins;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Allowlist read per-call, unlike other config

CLEARFOLIO_ARTIFACT_ORIGINS is read from process.env on every artifactUrl call, while CF_URL_INPUT and CF_SECRET are captured once at module load. Functionally fine since server env is fixed before startup, but the mixed timing is inconsistent and can surprise future maintainers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae
seonghobae marked this pull request as draft August 26, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: medium Normal-priority or P2 work status: draft Draft pull request type: bug Defect or incorrect behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants