Addresses the remaining medium findings from the second-pass audit:
- DoS hardening: bound GraphQL query cost with FixedComplexityLimit, and
reject non-positive items-per-page / page values so uint64 conversion
cannot underflow into an unbounded LIMIT/OFFSET. The -1 "load all"
sentinel stays valid for dashboards; REST now returns 400 for bad input.
- Security headers: add X-Content-Type-Options, X-Frame-Options,
Referrer-Policy and a conservative CSP (frame-ancestors/object-src/
base-uri) that hardens against clickjacking and base-tag injection
without restricting the self-hosted SPA's inline scripts.
- Stored XSS: render job.metaData.message as escaped text instead of
{@html ...} in Job.root and JobFootprint, preserving line breaks via
white-space: pre-wrap.
- SQL injection hardening: parameterize the tag-scope IN list and the
manager project subquery in CountTags instead of interpolating
user.Username / user.Projects (externally sourced via OIDC/LDAP).
- CSRF defense-in-depth: reject cross-site state-changing requests via
Sec-Fetch-Site, failing open for non-browser clients, on top of the
existing SameSite=Lax session cookie.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: de7d47a85c7c
A second-pass audit surfaced three severe issues missed by the previous
review, each a sibling code path of a bug class that was only partially
fixed before:
- auth: JWT session login (jwtSession.go) registered its authenticator
even when CROSS_LOGIN_JWT_HS512_KEY was unset, leaving an empty HMAC
key. golang-jwt verifies any HS256/HS512 signature against an empty
key, allowing unauthenticated admin token forgery. Init() now refuses
to register without a key, with a defense-in-depth empty-key guard in
the keyfunc.
- repository: metric names from GraphQL ([String!]) were interpolated
raw into json_extract(footprint, "$.<name>") SQL. SQLite parses
double-quoted strings as literals, enabling SQL injection by any
authenticated user. Validate metric names against ^[a-zA-Z0-9_]+$ in
jobsMetricStatisticsHistogram and buildFloatJSONCondition.
- metricstore: cluster/host line-protocol tags flowed unvalidated into
path.Join(RootDir, cluster, host) for checkpoint/WAL files, allowing
arbitrary file write outside the checkpoint root via NATS
(unauthenticated) or POST /api/write. Reject path-traversal sequences
in DecodeLine before the tags become path components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b57246993ec1
- Add migration 14: partial covering indexes WHERE job_state='running'
for user/project/subcluster groupings (tiny B-tree vs full table)
- Inline literal state value in BuildWhereClause so SQLite matches
partial indexes instead of parameterized placeholders
- Add per-request statsGroupCache (sync.Once per filter+groupBy key)
so identical grouped stats queries execute only once per GQL operation
- Parallelize 4 histogram queries in AddHistograms using errgroup
- Consolidate frontend from 6 GQL aliases to 2, sort+slice top-10
client-side via $derived
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 5b26a6e5ff10
Adds composite covering indexes on (cluster, job_state, <group_col>, ...)
for user, project, and subcluster groupings to enable index-only scans
for status views. Drops subsumed 3-column indexes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 3d8def28e96e