Fix critical issues from follow-up security audit

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
This commit is contained in:
2026-06-04 19:07:20 +02:00
parent 6f7e262f3f
commit 6d86690c76
4 changed files with 58 additions and 7 deletions

View File

@@ -22,6 +22,7 @@ import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
@@ -196,6 +197,16 @@ var decodeStatePool = sync.Pool{
// When the checkpoint format is "wal" each successfully decoded sample is also
// sent to WALMessages so the WAL staging goroutine can persist it durably
// before the next binary snapshot.
// isValidPathComponent reports whether s is safe to use as a single filesystem
// path component (cluster or host name) when building checkpoint/WAL paths. It
// rejects path-traversal sequences and embedded separators. An empty string is
// allowed because a missing host tag is legitimate and does not escape the root.
func isValidPathComponent(s string) bool {
return !strings.Contains(s, "..") &&
!strings.Contains(s, "/") &&
!strings.Contains(s, "\\")
}
func DecodeLine(dec *lineprotocol.Decoder,
ms *MemoryStore,
clusterDefault string,
@@ -282,6 +293,17 @@ func DecodeLine(dec *lineprotocol.Decoder,
}
}
// cluster and host are taken verbatim from attacker-controllable line
// protocol tags and are later used as filesystem path components for the
// checkpoint/WAL files (path.Join(RootDir, cluster, host)). Reject
// path-traversal sequences so a malicious tag cannot escape the
// checkpoint root. Skip the offending sample; the rest of the batch is
// still processed.
if !isValidPathComponent(cluster) || !isValidPathComponent(host) {
cclog.Warnf("[METRICSTORE]> rejecting metric with invalid cluster/host tag (cluster=%q host=%q)", cluster, host)
continue
}
// If the cluster or host changed, the lvl was set to nil
if lvl == nil {
st.selector = st.selector[:2]