Skip to content

fix: prevent STQL injection and reachable panics in service check/discovery - #145

Merged
joshiste merged 2 commits into
mainfrom
fix/stql-injection-and-panics
Jun 26, 2026
Merged

fix: prevent STQL injection and reachable panics in service check/discovery#145
joshiste merged 2 commits into
mainfrom
fix/stql-injection-and-panics

Conversation

@joshiste

Copy link
Copy Markdown
Member

Addresses the stackstate audit MAJOR findings.

1. STQL / JSON injection (MAJOR)

GetServiceSnapshot interpolated the service id raw into an STQL expression (id = "<id>"), and executeSnapshotQuery then hand-built the JSON request body with "query": "%v". A service id (a target attribute) containing quotes/backslashes could break out of the STQL string literal and/or corrupt the JSON body.

Fix:

  • stqlString(serviceId) renders the id as an escaped string literal (via JSON string escaping) so it can't break out of the STQL literal.
  • executeSnapshotQuery now JSON-encodes the query value into the body ("query": %s) instead of raw interpolation. The static metadata envelope is unchanged (it has no dynamic values — confirmed the body template has exactly one format verb).

2. Reachable panics (MAJOR)

  • Components[0] indexed without an empty check → now returns a clear error when StackState returns no components.
  • service.Identifiers[0] indexed without a check → now guarded (URL attribute degrades to empty).
  • ApiBaseUrl[:len-3] could underflow on a short URL → now length-guarded.
  • service_discovery.toService stripped the cluster/namespace URN prefixes with manual slice offsets that panic on an unexpected identifier (crashing the background discovery refresh) → now strings.TrimPrefix.
  • res.String() was called on the transport-error path where resty can return a nil response → removed there (kept on the !IsSuccess() path where res is non-nil).

Testing

  • go build ./..., go vet, gofmt clean.
  • New extservice/common_test.go: stqlString escaping, and a malicious service id (1") OR (1=1) keeps the request body valid JSON and stays inside a single escaped STQL literal.
  • go test ./extservice/ passes (existing mocked tests + new ones).

/simplify

4-agent pass: reuse/simplification/efficiency clean. Altitude confirmed the body template has no other dynamic value (injection fully closed) and the other guards are right-depth. Two notes:

  • Softened the stqlString comment to not overclaim general STQL escape-semantics (the ids in practice are numeric/URN, so it's effective regardless).
  • Follow-up (not in this PR): the ApiBaseUrl[:len-3] chop is kept behavior-preserving (length-guarded) here, but it leaves a latent issue — for the documented …/api base URL it strips to …/ and the built UI link gets a double slash. A cleaner fix is strings.TrimSuffix(ApiBaseUrl, "/api"). Left out to avoid a behavior change in this security PR; happy to do it as a follow-up.

…covery

The service snapshot query interpolated the service id raw into an STQL
expression (id = "<id>") which was then hand-formatted into a JSON
request body. A service id containing quotes/backslashes could break out
of the STQL string literal and/or corrupt the JSON body. Escape the id
as an STQL string literal and build the body by JSON-encoding the query
value, closing the injection at both layers.

Also harden several reachable panics:
- guard against an empty Components slice before indexing [0] in the
  service status check
- guard service.Identifiers and a short ApiBaseUrl before indexing in
  toMetric
- use strings.TrimPrefix instead of manual slice offsets when parsing
  cluster/namespace identifiers in discovery (the old form could panic
  on an unexpected identifier and crash the discovery refresh)
- stop calling res.String() on the transport-error path, where resty can
  return a nil response
@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @joshiste's task in 2m 2s —— View job


PR Review

  • Gather context and read changed files
  • Check for previously unresolved review threads (none found)
  • Review new changes
  • Post final summary

Summary

The fixes are correct and well-scoped. No prior review threads to resolve.

STQL/JSON injection fix (common.go)

stqlString uses json.Marshal to produce a properly-quoted string literal. executeSnapshotQuery now JSON-encodes the entire query string before splicing it into the body template. Both layers close the injection correctly. The test validates the end-to-end path with a payload designed to escape the STQL literal.

One minor note: json.Marshal on a Go string never returns an error (invalid UTF-8 is replaced, not rejected), so the if err != nil guard on lines 52–54 of common.go is unreachable dead code. Not a bug, just slightly misleading. Leaving it out or using //nolint would be cleaner, but it's harmless as-is.

Panic guards (service_check.go, service_discovery.go)

  • Components[0] → guarded with a length check, returns a clear error. ✓
  • service.Identifiers[0] → guarded, serviceUrl falls back to "". ✓
  • ApiBaseUrl[:len-3] → length-guarded against underflow. ✓
  • Manual slice offsets in toServicestrings.TrimPrefix is correct and panic-safe. ✓
  • res.String() on the err != nil transport-error path → removed in both service_check.go (commit 55c6f2a) and service_discovery.go (commit 5a7608e, the follow-up commit). ✓

Tests (common_test.go)

Good coverage: unit test for stqlString and an integration-style test that confirms the captured request body is valid JSON and the malicious ID doesn't escape the STQL literal.

No blocking issues. The PR is well-reasoned and addresses all five audit findings cleanly.

getAllServices called res.String() on the transport-error path, where
resty can return a nil response (the same issue fixed in the service
check). Drop res.String() there too.
@joshiste

Copy link
Copy Markdown
Member Author

Good catch — fixed in 5a7608e. getAllServices in service_discovery.go had the same res.String()-on-transport-error nil-deref that the service check did; I'd missed it (and the PR description over-claimed). Dropped res.String() from that error-path log too. The non-error StatusCode() != 200 branch keeps it (there res is non-nil).

The other two notes are intentionally left as-is: stqlString ignores the json.Marshal error because marshaling a string cannot fail, and the ApiBaseUrl TrimSuffix("/api") cleanup is tracked as a follow-up (kept behavior-preserving here to avoid a URL-construction change in a security fix).

@sonarqubecloud

Copy link
Copy Markdown

@joshiste
joshiste merged commit abdb6db into main Jun 26, 2026
14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant