Skip to content

🐛 Harden card proxy: CIDR fatal, context propagation, AbortController#4237

Merged
clubanderson merged 1 commit intomainfrom
fix/card-proxy-hardening
Apr 2, 2026
Merged

🐛 Harden card proxy: CIDR fatal, context propagation, AbortController#4237
clubanderson merged 1 commit intomainfrom
fix/card-proxy-hardening

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

@clubanderson clubanderson commented Apr 2, 2026

  • Understand existing code and test patterns
  • Fix useCardFetch.ts: use name-based AbortError check
  • Fix useCardFetch.ts: handle 204/empty body responses
  • Fix card_proxy.go: update audit trail log comment
  • Fix card_proxy.go: truncate/quote redirect Location header in log
  • Make CardProxyHandler http.Client injectable (enables redirect tests)
  • Add unit tests for useCardFetch.ts (abort, localStorage SecurityError, 204/empty body, non-JSON 200, success, skip, null URL)
  • Add handler-level tests for card_proxy.go redirect detection (301/302/307, long location, missing URL, blocked scheme, localhost)
  • Run build and lint (pass)
  • Run Go tests (pass)
  • Run Vitest (9/9 pass)

Follow-up to #4230 addressing code review and silent-failure findings:

Backend (card_proxy.go):
- CIDR parse failure now log.Fatalf instead of silent skip
- Use http.NewRequestWithContext for client disconnect cancellation
- Detect 3xx redirects and return helpful error instead of opaque status
- Add logging on all error paths + audit log on success
- Log response size on oversized body rejection

Frontend (useCardFetch.ts):
- Add AbortController to cancel in-flight fetches on URL change/unmount
- Guard localStorage.getItem against SecurityError in sandboxed iframes
- Set loading=false when concurrency limit is hit
- Wrap res.json() with helpful error for non-JSON 200 responses

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Copilot AI review requested due to automatic review settings April 2, 2026 13:22
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 2, 2026
@clubanderson
Copy link
Copy Markdown
Collaborator Author

/lgtm
/approve

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 2, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit caea8ab
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69ce6d88cca56e0008d87200
😎 Deploy Preview https://deploy-preview-4237.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@kubestellar-prow
Copy link
Copy Markdown
Contributor

@clubanderson: you cannot LGTM your own PR.

Details

In response to this:

/lgtm
/approve

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Apr 2, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: clubanderson

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Apr 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Hardens the Tier 2 card data-fetching pathway (frontend hook + backend proxy) to reduce silent failures, improve cancellation behavior, and make upstream errors more actionable.

Changes:

  • Backend card proxy now fails fast on blocked-CIDR parse errors, ties upstream requests to client context, and provides explicit redirect errors with improved logging/audit output.
  • Frontend useCardFetch now cancels in-flight requests via AbortController, safely reads auth token from localStorage, and emits clearer errors for non-JSON success responses.
  • Fixes a stuck-loading edge case when per-card concurrency limits are hit.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
web/src/lib/dynamic-cards/useCardFetch.ts Adds safe token retrieval, request cancellation, and improved JSON parsing/error handling for the sandbox fetch hook.
pkg/api/handlers/card_proxy.go Hardens SSRF boundary initialization, propagates request context, detects redirects explicitly, and improves logging/auditability.

})
.catch(err => {
// Ignore abort errors — expected when URL changes or card unmounts
if (err instanceof DOMException && err.name === 'AbortError') return
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Abort handling relies on err instanceof DOMException, which can miss AbortError cases where the rejection is an Error (e.g., older Safari / polyfills). Elsewhere in the codebase the convention is to check (err as { name?: string })?.name === 'AbortError' to handle both Error and DOMException (see web/src/hooks/mcp/events.ts:176-178). Consider switching to a name-based check here as well so aborted fetches don’t surface as user-visible errors.

Suggested change
if (err instanceof DOMException && err.name === 'AbortError') return
const isAbortError = (err as { name?: string } | null)?.name === 'AbortError'
if (isAbortError) return

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +117
return res.json().catch(() => {
throw new Error(
'Response is not valid JSON. The external API may be returning HTML, XML, or plain text.',
)
})
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

On the success path, res.json() will also fail for legitimate empty responses (e.g., HTTP 204 No Content, or 200 with an empty body), and this currently gets turned into the generic “not valid JSON” error. Consider special-casing res.status === 204 and/or an empty body to return null (or {}) without error, so APIs that legitimately return no content don’t break.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +41
/** Safely read from localStorage — returns null if unavailable (sandboxed iframes, etc.) */
function safeGetToken(): string | null {
try {
return localStorage.getItem(STORAGE_KEY_TOKEN)
} catch {
return null
}
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The new behaviors (safe localStorage access, AbortController cancellation, and non-JSON response messaging) don’t appear to have unit test coverage. Since this package already has Vitest coverage for other sandbox scopes (e.g., web/src/lib/dynamic-cards/tests/scope.test.ts), consider adding tests for: (1) aborts being ignored, (2) localStorage SecurityError returning null token, and (3) empty/204 or non-JSON 200 handling.

Copilot uses AI. Check for mistakes.
})
}

// Log successful proxy requests for audit trail
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The comment says “Log successful proxy requests for audit trail”, but this log line will run for any non-redirect response including 4xx/5xx from the upstream. Either adjust the comment to reflect that it logs all proxied responses, or gate the log to 2xx if “successful” is meant literally.

Suggested change
// Log successful proxy requests for audit trail
// Log proxied responses for audit trail

Copilot uses AI. Check for mistakes.
// Detect redirects and return a helpful error instead of an opaque 3xx
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
log.Printf("[CardProxy] Redirect from %s (status %d, location=%s)", host, resp.StatusCode, location)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

location comes from an upstream header and can be very long or contain unexpected characters; logging it unquoted with %s can produce noisy/misleading logs. Consider logging it with %q and optionally truncating to a reasonable length to keep logs safe and readable.

Suggested change
log.Printf("[CardProxy] Redirect from %s (status %d, location=%s)", host, resp.StatusCode, location)
safeLocation := location
const maxLocationLogLen = 200
if len(safeLocation) > maxLocationLogLen {
safeLocation = safeLocation[:maxLocationLogLen] + "...(truncated)"
}
log.Printf("[CardProxy] Redirect from %s (status %d, location=%q)", host, resp.StatusCode, safeLocation)

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +189
// Detect redirects and return a helpful error instead of an opaque 3xx
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
log.Printf("[CardProxy] Redirect from %s (status %d, location=%s)", host, resp.StatusCode, location)
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{
"error": fmt.Sprintf("External API returned a redirect (%d). Update the URL to the final destination.", resp.StatusCode),
})
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The redirect-detection behavior is new and security-relevant (ensures callers don’t silently accept 3xx). There’s currently only unit coverage for isBlockedIP; consider adding handler-level tests that assert 3xx responses return the expected JSON error and status, using the existing Fiber test patterns in pkg/api/handlers/*_test.go.

Copilot uses AI. Check for mistakes.
@clubanderson
Copy link
Copy Markdown
Collaborator Author

🔄 Auto-Applying Copilot Code Review

Copilot code review found 3 code suggestion(s) and 3 general comment(s).

@copilot Please apply all of the following code review suggestions:

  • web/src/lib/dynamic-cards/useCardFetch.ts (line 126): const isAbortError = (err as { name?: string } | null)?.name === 'AbortError' ...
  • pkg/api/handlers/card_proxy.go (line 207): // Log proxied responses for audit trail
  • pkg/api/handlers/card_proxy.go (line 185): safeLocation := location const maxLocationLogLen = 200 if len(safeLocation) ...

Also address these general comments:

  • web/src/lib/dynamic-cards/useCardFetch.ts (line 117): On the success path, res.json() will also fail for legitimate empty responses (e.g., HTTP 204 No Content, or 200 with
  • web/src/lib/dynamic-cards/useCardFetch.ts (line 41): The new behaviors (safe localStorage access, AbortController cancellation, and non-JSON response messaging) don’t appear
  • pkg/api/handlers/card_proxy.go (line 189): The redirect-detection behavior is new and security-relevant (ensures callers don’t silently accept 3xx). There’s curren

Push all fixes in a single commit. Run cd web && npm run build && npm run lint before committing.


Auto-generated by copilot-review-apply workflow.

@clubanderson clubanderson merged commit 114efe6 into main Apr 2, 2026
27 of 30 checks passed
@kubestellar-prow kubestellar-prow bot deleted the fix/card-proxy-hardening branch April 2, 2026 13:29
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

🔄 Auto-Applying Copilot Code Review

Copilot code review found 3 code suggestion(s) and 3 general comment(s)....

All suggestions applied in commit 3ea6d1e:

Code suggestions:

  • useCardFetch.ts: AbortError check now uses (err as { name?: string } | null)?.name === 'AbortError' — handles both DOMException and plain Error across all browsers/polyfills
  • card_proxy.go: Comment updated to "Log proxied responses for audit trail"
  • card_proxy.go: Redirect Location header now logged with %q and truncated to 200 chars

General comments addressed:

  • useCardFetch.ts: HTTP 204 and empty body responses now return null without error (replaced res.json() with res.text() + JSON.parse to handle all cases cleanly)
  • useCardFetch.ts: Added 9 unit tests covering abort silence, localStorage SecurityError, 204/empty body, non-JSON 200, and happy path
  • card_proxy.go: Made http.Client injectable on CardProxyHandler struct; added 5 handler-level tests covering 301/302/307 redirect detection, long Location truncation, missing URL, blocked scheme, and localhost blocking

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has signed the DCO. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants