Skip to content

🐛 Fix 3 security issues: scale auth, SSE cache namespace, JSON validation#4157

Merged
clubanderson merged 1 commit intomainfrom
fix/security-batch
Apr 1, 2026
Merged

🐛 Fix 3 security issues: scale auth, SSE cache namespace, JSON validation#4157
clubanderson merged 1 commit intomainfrom
fix/security-batch

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Fixes three security vulnerabilities reported in #4150, #4151, and #4156:

  • Unauthenticated scale endpoint accepts GET mutations #4150 — Unauthenticated scale endpoint: Added validateToken check to /scale and removed GET-based mutations (POST-only now). Previously, the scale endpoint accepted unauthenticated requests and allowed mutations via GET query parameters, enabling CSRF-style attacks. Moved /scale from endpointsLackingAuth to sensitiveEndpoints in the auth test suite.

  • SSE cache key ignores namespace filter, causes cross-namespace data leakage #4151 — SSE cache key ignores namespace: Added namespace field to sseClusterStreamConfig and included it in the cache key (demoKey:cluster:namespace). Previously, cache keys were only demoKey:cluster, so requests for different namespaces on the same cluster could return stale data from a previous namespace query.

  • Malformed JSON bodies are silently accepted on mutation/trigger paths #4156 — Malformed JSON silently accepted: Added JSON decode error checking on /auto-update/trigger and /predictions/analyze. Previously, decode errors were discarded with _, causing malformed JSON payloads to produce zero-value structs and proceed with operations instead of returning 400.

Test plan

  • All TestEndpointAuth_* tests pass — /scale now correctly rejects unauthenticated/bad-token requests
  • TestScaleWorkload handler test passes
  • TestPredictionWorker and TestPredictionMetrics pass
  • All pkg/api/handlers tests pass (SSE cache key change is backward-compatible)
  • go build ./pkg/... succeeds with no compilation errors

Closes #4150, closes #4151, closes #4156

…tion (#4150, #4151, #4156)

- #4150: Add validateToken check to /scale endpoint and reject GET mutations
  (POST-only) to prevent unauthenticated CSRF-style scaling attacks
- #4151: Include namespace in SSE cache key to prevent cross-namespace data
  leakage when the same cluster is queried for different namespaces
- #4156: Validate JSON decode errors on /auto-update/trigger and
  /predictions/analyze instead of silently accepting malformed bodies

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

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mikespreitzer for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@clubanderson clubanderson merged commit cccae84 into main Apr 1, 2026
16 checks passed
@kubestellar-prow kubestellar-prow bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Apr 1, 2026
@kubestellar-prow kubestellar-prow bot deleted the fix/security-batch branch April 1, 2026 14:09
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

Deploy Preview for kubestellarconsole canceled.

Name Link
🔨 Latest commit 8a0f91a
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cd270b5618640008cdd70c

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

Addresses three reported security vulnerabilities by tightening agent auth/method handling for scaling, preventing cross-namespace SSE cache collisions, and rejecting malformed JSON bodies that were previously silently accepted.

Changes:

  • Require auth + POST-only semantics for the agent /scale handler; update auth test categorization.
  • Include namespace in SSE cache keys to avoid cross-namespace stale/cross-leaked results.
  • Add explicit JSON decode error handling to /auto-update/trigger and /predictions/analyze.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
pkg/api/handlers/sse.go Adds namespace to SSE stream config and incorporates it into the cache key.
pkg/agent/server.go Enforces auth + POST-only for scaling; rejects malformed JSON for auto-update trigger and predictions analyze.
pkg/agent/endpoint_auth_test.go Moves /scale into the “sensitive endpoints” list and clears the “lacking auth” list.

Comment on lines +1964 to +1968
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON body"})
return
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

r.ContentLength != 0 will skip validation when ContentLength == -1 (unknown length, e.g. chunked transfer). That reintroduces the “malformed JSON silently accepted” behavior for some requests. Prefer attempting a decode whenever r.Body is present (or r.Body != http.NoBody) and treating io.EOF as “empty body” rather than gating on ContentLength.

Suggested change
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON body"})
return
if r.Body != nil && r.Body != http.NoBody {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
if err != io.EOF {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON body"})
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +4237 to 4242
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Same issue as above: the ContentLength != 0 guard will skip decoding when ContentLength == -1, allowing malformed JSON to bypass the new check. Decode when the body is present and treat io.EOF as an empty/absent body.

Copilot uses AI. Check for mistakes.
json.NewDecoder(r.Body).Decode(&req)
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

http.Error returns a plain-text response (and sets Content-Type: text/plain), which can be inconsistent with the rest of this handler/endpoint family that appears to return JSON objects for errors. Consider returning a JSON error payload here (and setting status) for consistency with other agent endpoints.

Copilot uses AI. Check for mistakes.
Comment on lines +1540 to +1547
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": "POST required",
})
return
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

For 405 Method Not Allowed, it’s best practice to include an Allow header advertising the permitted method(s) (e.g., POST). This improves client behavior and aligns with HTTP semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +22
// namespace is the optional namespace filter. When set it is included in
// the cache key so that requests for different namespaces on the same
// cluster do not return stale cross-namespace data (#4151).
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The comment says “When set it is included in the cache key”, but the implementation always appends cfg.namespace (even when empty). Either adjust the comment to reflect that it’s always included, or conditionally include the namespace segment only when it’s non-empty.

Suggested change
// namespace is the optional namespace filter. When set it is included in
// the cache key so that requests for different namespaces on the same
// cluster do not return stale cross-namespace data (#4151).
// namespace is the optional namespace filter. Its value (including empty)
// is included in the cache key so that requests for different namespaces on
// the same cluster do not return stale cross-namespace data (#4151).

Copilot uses AI. Check for mistakes.
clubanderson added a commit that referenced this pull request Apr 1, 2026
- server.go: Remove ContentLength!=0 guard — use Body!=nil with
  io.EOF tolerance for empty bodies. JSON error response instead of
  plain text on /predictions/analyze (#4157)
- sse.go: Fix comment to reflect namespace is always in cache key
  (#4157)
- gateway.go: Use errors.Join for unwrappable error chains (#4159)

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
clubanderson added a commit that referenced this pull request Apr 1, 2026
- server.go: Remove ContentLength!=0 guard — use Body!=nil with
  io.EOF tolerance for empty bodies. JSON error response instead of
  plain text on /predictions/analyze (#4157)
- sse.go: Fix comment to reflect namespace is always in cache key
  (#4157)
- gateway.go: Use errors.Join for unwrappable error chains (#4159)

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

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

Check out what's new:

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 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.

@clubanderson
Copy link
Copy Markdown
Collaborator Author

🔄 Auto-Applying Copilot Code Review

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

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

  • pkg/agent/server.go (line 1968): if r.Body != nil && r.Body != http.NoBody { if err := json.NewDecoder(r.Body)....
  • pkg/api/handlers/sse.go (line 22): // namespace is the optional namespace filter. Its value (including empty) // i...

Also address these general comments:

  • pkg/agent/server.go (line 4242): Same issue as above: the ContentLength != 0 guard will skip decoding when ContentLength == -1, allowing malformed JS
  • pkg/agent/server.go (line 4239): http.Error returns a plain-text response (and sets Content-Type: text/plain), which can be inconsistent with the res
  • pkg/agent/server.go (line 1547): For 405 Method Not Allowed, it’s best practice to include an Allow header advertising the permitted method(s) (e.g.,

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.

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

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

3 participants