Fix medium-severity issues from follow-up security audit

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

diff --git a/cmd/cc-backend/server.go b/cmd/cc-backend/server.go
index 33504988..f39cd45d 100644
--- a/cmd/cc-backend/server.go
+++ b/cmd/cc-backend/server.go
@@ -23,6 +23,7 @@ import (

 	"github.com/99designs/gqlgen/graphql"
 	"github.com/99designs/gqlgen/graphql/handler"
+	"github.com/99designs/gqlgen/graphql/handler/extension"
 	"github.com/99designs/gqlgen/graphql/handler/transport"
 	"github.com/99designs/gqlgen/graphql/playground"
 	"github.com/ClusterCockpit/cc-backend/internal/api"
@@ -50,6 +51,12 @@ const (
 	envDebug = "DEBUG"
 )

+// maxQueryComplexity bounds the cost of a single GraphQL query to mitigate
+// denial-of-service via deeply nested or heavily aliased queries. The default
+// per-field cost is 1, so this leaves ample headroom for legitimate dashboard
+// queries while rejecting pathological ones.
+const maxQueryComplexity = 5000
+
 // Server encapsulates the HTTP server state and dependencies
 type Server struct {
 	router        chi.Router
@@ -90,6 +97,7 @@ func (s *Server) init() error {
 		generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))

 	graphQLServer.AddTransport(transport.POST{})
+	graphQLServer.Use(extension.FixedComplexityLimit(maxQueryComplexity))

 	// Inject a per-request stats cache so that grouped statistics queries
 	// sharing the same (filter, groupBy) pair are executed only once.
@@ -136,8 +144,38 @@ func (s *Server) init() error {
 	}))
 	s.router.Use(func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+			h := rw.Header()
+			h.Set("X-Content-Type-Options", "nosniff")
+			h.Set("X-Frame-Options", "DENY")
+			h.Set("Referrer-Policy", "same-origin")
+			// Conservative CSP: blocks clickjacking (frame-ancestors), plugin
+			// content (object-src) and <base> injection (base-uri) without
+			// restricting scripts/styles, so it cannot break the self-hosted
+			// SPA which relies on inline scripts. A full script-src policy needs
+			// per-template nonces and should be added separately.
+			h.Set("Content-Security-Policy", "frame-ancestors 'none'; object-src 'none'; base-uri 'self'")
 			if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
-				rw.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+				h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")


Bash

git show 6f7e262f --format="%B" | head -60

Show commit message for first security fixes


Fix issues after security audit

Entire-Checkpoint: bc18358a9343

diff --git a/CLAUDE.md b/CLAUDE.md
index 061d056b..fe6a7aab 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -314,6 +314,24 @@ job,function=stop_job event="{\"jobId\":123,\"cluster\":\"test\",\"startTime\":1
 - Messages are logged; no responses are sent back to publishers
 - If NATS client is unavailable, API subscriptions are skipped (logged as warning)

+### Security Considerations
+
+**The NATS API has no application-layer authentication or authorization.** Unlike
+the REST endpoints (which require a JWT with `RoleAPI`), the subscribers process
+any message delivered on the configured subjects. Anyone with publish rights to
+those subjects on the broker can:
+
+- Insert arbitrary jobs (potentially attributed to other users)
+- Mark running jobs as stopped, triggering archive/finalization
+- Overwrite node state and health metadata for any cluster
+
+Operators MUST restrict publish ACLs at the NATS broker (per-account or
+per-subject permissions) so that only trusted producers — e.g. the scheduler
+integration on a known host or service account — can publish to the configured
+`subject-job-event` and `subject-node-state` subjects. A shared, unrestricted
+NATS broker is not a safe deployment topology for this API. A startup warning
+is logged when these subscriptions are enabled.
+
 ## Development Guidelines

 ### Performance
diff --git a/cmd/cc-backend/server.go b/cmd/cc-backend/server.go
index 85503535..33504988 100644
--- a/cmd/cc-backend/server.go
+++ b/cmd/cc-backend/server.go
@@ -129,11 +129,19 @@ func (s *Server) init() error {
 	s.router.Use(middleware.Compress(5))
 	s.router.Use(middleware.Recoverer)
 	s.router.Use(cors.Handler(cors.Options{
-		AllowCredentials: true,
+		AllowCredentials: false,
 		AllowedHeaders:   []string{"X-Requested-With", "Content-Type", "Authorization", "Origin"},
 		AllowedMethods:   []string{"GET", "POST", "HEAD", "OPTIONS"},
 		AllowedOrigins:   []string{"*"},
 	}))
+	s.router.Use(func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+			if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
+				rw.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+			}
+			next.ServeHTTP(rw, r)
+		})
+	})

 	s.restAPIHandle = api.New()

diff --git a/internal/api/api_test.go b/internal/api/api_test.go


Update the release notes with latest changes and adopt README after recent changes as necessary


Now I have a clear picture of all security changes. Let me check the current version strings in code.


grep -r "version=1.5" Makefile cmd/ 2>/dev/null | head -10

Find version strings in Makefile and main command


(Bash completed with no output)


grep -r "1\.5\.[0-9]" Makefile cmd/ internal/api/docs.go 2>/dev/null | head -10

Find version references


Makefile:VERSION = 1.5.4


grep -n "VERSION\|version" Makefile | head -20

Show version-related lines in Makefile


3:VERSION = 1.5.4
6:LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'


grep -rn "1\.5\.4\|v1\.5\.4" internal/api/docs.go 2>/dev/null | head -5

Check API docs for version


1	TARGET = ./cc-backend
2	FRONTEND = ./web/frontend
3	VERSION = 1.5.4
4	GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
5	CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
6	LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'
7
8	EXECUTABLES = go npm
9	K := $(foreach exec,$(EXECUTABLES),\
10	        $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
11
12	SVELTE_COMPONENTS = status   \
13						analysis \
14						node     \
15						systems  \
16						job      \
17						list     \
18						user     \
19						jobs     \
20						header

TARGET = ./cc-backend
FRONTEND = ./web/frontend
VERSION = 1.5.4
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'

EXECUTABLES = go npm
K := $(foreach exec,$(EXECUTABLES),\
        $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))

SVELTE_COMPONENTS = status   \
					analysis \
					node     \
					systems  \
					job      \
					list     \
					user     \
					jobs     \
					header


Now I have everything I need. This is a security release that should be version 1.5.5. Let me update the Makefile and ReleaseNotes.


The file /Users/jan/prg/ClusterCockpit/cc-backend/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)

TARGET = ./cc-backend
FRONTEND = ./web/frontend
VERSION = 1.5.4
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'

EXECUTABLES = go npm
K := $(foreach exec,$(EXECUTABLES),\
        $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))

SVELTE_COMPONENTS = status   \
					analysis \
					node     \
					systems  \
					job      \
					list     \
					user     \
					jobs     \
					header

SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .js, $(SVELTE_COMPONENTS)))
SVELTE_SRC = $(wildcard $(FRONTEND)/src/*.svelte)                 \
			 $(wildcard $(FRONTEND)/src/*.js)                     \
			 $(wildcard $(FRONTEND)/src/analysis/*.svelte)        \
			 $(wildcard $(FRONTEND)/src/config/*.svelte)          \
			 $(wildcard $(FRONTEND)/src/config/admin/*.svelte)    \
			 $(wildcard $(FRONTEND)/src/config/user/*.svelte)     \
			 $(wildcard $(FRONTEND)/src/generic/*.js)             \
			 $(wildcard $(FRONTEND)/src/generic/*.svelte)         \
			 $(wildcard $(FRONTEND)/src/generic/filters/*.svelte) \
			 $(wildcard $(FRONTEND)/src/generic/plots/*.svelte)   \
			 $(wildcard $(FRONTEND)/src/generic/joblist/*.svelte) \
			 $(wildcard $(FRONTEND)/src/generic/helper/*.svelte)  \
			 $(wildcard $(FRONTEND)/src/generic/select/*.svelte)  \
			 $(wildcard $(FRONTEND)/src/header/*.svelte)          \
			 $(wildcard $(FRONTEND)/src/job/*.svelte)

.PHONY: clean distclean fmt lint test tags frontend swagger graphql $(TARGET)

.NOTPARALLEL:

$(TARGET): $(SVELTE_TARGETS)
	$(info ===>  BUILD cc-backend)
	@go build -ldflags=${LD_FLAGS} ./cmd/cc-backend

frontend:
	$(info ===>  BUILD frontend)
	cd web/frontend && npm ci && npm run build

swagger:
	$(info ===>  GENERATE swagger)
	@go tool github.com/swaggo/swag/cmd/swag init  --parseDependency -d ./internal/api -g rest.go -o ./api
	@mv ./api/docs.go ./internal/api/docs.go

graphql:
	$(info ===>  GENERATE graphql)
	@go tool github.com/99designs/gqlgen

clean:
	$(info ===>  CLEAN)
	@go clean
	@rm -f $(TARGET)

distclean:
	@$(MAKE) clean
	$(info ===>  DISTCLEAN)
	@rm -rf $(FRONTEND)/node_modules
	@rm -rf ./var

test:
	$(info ===>  TESTING)
	@go clean -testcache
	@go build ./...
	@go vet ./...
	@go test ./...

fmt:
	$(info ===>  FORMAT)
	@gofumpt -l -w .

lint:
	$(info ===>  LINT)
	@golangci-lint run ./...

tags:
	$(info ===>  TAGS)
	@ctags -R

$(VAR):
	@mkdir -p $(VAR)

$(SVELTE_TARGETS): $(SVELTE_SRC)
	$(info ===>  BUILD frontend)
	cd web/frontend && npm ci && npm run build


Now update the ReleaseNotes.md with a new 1.5.5 section:


[Request interrupted by user]


1.5.4 is not yet released. The version can stay there


Reverting the Makefile change.


TARGET = ./cc-backend
FRONTEND = ./web/frontend
VERSION = 1.5.5
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'

EXECUTABLES = go npm
K := $(foreach exec,$(EXECUTABLES),\
        $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))

SVELTE_COMPONENTS = status   \
					analysis \
					node     \
					systems  \
					job      \
					list     \
					user     \
					jobs     \
					header

SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .js, $(SVELTE_COMPONENTS)))
SVELTE_SRC = $(wildcard $(FRONTEND)/src/*.svelte)                 \
			 $(wildcard $(FRONTEND)/src/*.js)                     \
			 $(wildcard $(FRONTEND)/src/analysis/*.svelte)        \
			 $(wildcard $(FRONTEND)/src/config/*.svelte)          \
			 $(wildcard $(FRONTEND)/src/config/admin/*.svelte)    \
			 $(wildcard $(FRONTEND)/src/config/user/*.svelte)     \
			 $(wildcard $(FRONTEND)/src/generic/*.js)             \
			 $(wildcard $(FRONTEND)/src/generic/*.svelte)         \
			 $(wildcard $(FRONTEND)/src/generic/filters/*.svelte) \
			 $(wildcard $(FRONTEND)/src/generic/plots/*.svelte)   \
			 $(wildcard $(FRONTEND)/src/generic/joblist/*.svelte) \
			 $(wildcard $(FRONTEND)/src/generic/helper/*.svelte)  \
			 $(wildcard $(FRONTEND)/src/generic/select/*.svelte)  \
			 $(wildcard $(FRONTEND)/src/header/*.svelte)          \
			 $(wildcard $(FRONTEND)/src/job/*.svelte)

.PHONY: clean distclean fmt lint test tags frontend swagger graphql $(TARGET)

.NOTPARALLEL:

$(TARGET): $(SVELTE_TARGETS)
	$(info ===>  BUILD cc-backend)
	@go build -ldflags=${LD_FLAGS} ./cmd/cc-backend

frontend:
	$(info ===>  BUILD frontend)
	cd web/frontend && npm ci && npm run build

swagger:
	$(info ===>  GENERATE swagger)
	@go tool github.com/swaggo/swag/cmd/swag init  --parseDependency -d ./internal/api -g rest.go -o ./api
	@mv ./api/docs.go ./internal/api/docs.go

graphql:
	$(info ===>  GENERATE graphql)
	@go tool github.com/99designs/gqlgen

clean:
	$(info ===>  CLEAN)
	@go clean
	@rm -f $(TARGET)

distclean:
	@$(MAKE) clean
	$(info ===>  DISTCLEAN)
	@rm -rf $(FRONTEND)/node_modules
	@rm -rf ./var

test:
	$(info ===>  TESTING)
	@go clean -testcache
	@go build ./...
	@go vet ./...
	@go test ./...

fmt:
	$(info ===>  FORMAT)
	@gofumpt -l -w .

lint:
	$(info ===>  LINT)
	@golangci-lint run ./...

tags:
	$(info ===>  TAGS)
	@ctags -R

$(VAR):
	@mkdir -p $(VAR)

$(SVELTE_TARGETS): $(SVELTE_SRC)
	$(info ===>  BUILD frontend)
	cd web/frontend && npm ci && npm run build


Now I'll add the security fixes to the 1.5.4 section in ReleaseNotes.md:


Edit

## Changes in 1.5.4

### Bug fixes

- **Roofline legend placement**: Roofline plot legends now use fixed

## Changes in 1.5.4

### Security fixes

- **JWT HMAC empty-key bypass (critical)**: `jwtSession.go` now refuses to
  register when `CROSS_LOGIN_JWT_HS512_KEY` is unset. Previously, an empty HMAC
  key allowed unauthenticated admin token forgery because `golang-jwt` verifies
  any HS256/HS512 signature against an empty key.
- **SQL injection via metric names (critical)**: Metric names supplied through
  GraphQL (`[String!]`) were interpolated raw into `json_extract` SQL expressions.
  Names are now validated against `^[a-zA-Z0-9_]+$` in
  `jobsMetricStatisticsHistogram` and `buildFloatJSONCondition`.
- **Path traversal via line-protocol tags (critical)**: `cluster` and `host`
  tags from the metric line protocol flowed unvalidated into `path.Join` for
  checkpoint/WAL file paths, enabling arbitrary file writes outside the
  checkpoint root via NATS (unauthenticated) or `POST /api/write`. Path-traversal
  sequences are now rejected in `DecodeLine` before the tags become path
  components.
- **CORS `AllowCredentials` disabled**: CORS middleware no longer sets
  `AllowCredentials: true`, which was incompatible with `AllowedOrigins: ["*"]`
  and could enable cross-origin credential theft.
- **HSTS header added**: `Strict-Transport-Security` is now set for all
  HTTPS connections.
- **Security response headers**: Added `X-Content-Type-Options: nosniff`,
  `X-Frame-Options: DENY`, `Referrer-Policy: same-origin`, and a conservative
  `Content-Security-Policy` (blocks `frame-ancestors`, `object-src`, `base-uri`)
  to harden against clickjacking and base-tag injection.
- **Stored XSS in job message**: `job.metaData.message` is now rendered as
  escaped text (CSS `white-space: pre-wrap`) instead of raw `{@html ...}` in
  `Job.root` and `JobFootprint`.
- **SQL injection in tag queries**: The tag-scope `IN` list and manager project
  subquery in `CountTags` are now parameterized instead of interpolating
  `user.Username` / `user.Projects` values sourced from OIDC/LDAP.
- **GraphQL DoS hardening**: Query cost is bounded with `FixedComplexityLimit`
  (5000). Non-positive `items-per-page` and `page` values are rejected with HTTP
  400 to prevent integer underflow into unbounded `LIMIT`/`OFFSET` queries.
- **CSRF defense-in-depth**: State-changing requests with a cross-site
  `Sec-Fetch-Site` header are now rejected (fails open for non-browser clients),
  complementing the existing `SameSite=Lax` session cookie.
- **NATS API security warning**: A startup warning is now logged when NATS
  subscriptions are enabled, reminding operators that the NATS API has no
  application-layer authentication and that publish ACLs must be restricted at
  the broker.

### Bug fixes

- **Roofline legend placement**: Roofline plot legends now use fixed


The file /Users/jan/prg/ClusterCockpit/cc-backend/ReleaseNotes.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Changes in 1.5.4

### Bug fixes

- **Roofline legend placement**: Roofline plot legends now use fixed

## Changes in 1.5.4

### Security fixes

- **JWT HMAC empty-key bypass (critical)**: `jwtSession.go` now refuses to
  register when `CROSS_LOGIN_JWT_HS512_KEY` is unset. Previously, an empty HMAC
  key allowed unauthenticated admin token forgery because `golang-jwt` verifies
  any HS256/HS512 signature against an empty key.
- **SQL injection via metric names (critical)**: Metric names supplied through
  GraphQL (`[String!]`) were interpolated raw into `json_extract` SQL expressions.
  Names are now validated against `^[a-zA-Z0-9_]+$` in
  `jobsMetricStatisticsHistogram` and `buildFloatJSONCondition`.
- **Path traversal via line-protocol tags (critical)**: `cluster` and `host`
  tags from the metric line protocol flowed unvalidated into `path.Join` for
  checkpoint/WAL file paths, enabling arbitrary file writes outside the
  checkpoint root via NATS (unauthenticated) or `POST /api/write`. Path-traversal
  sequences are now rejected in `DecodeLine` before the tags become path
  components.
- **CORS `AllowCredentials` disabled**: CORS middleware no longer sets
  `AllowCredentials: true`, which was incompatible with `AllowedOrigins: ["*"]`
  and could enable cross-origin credential theft.
- **HSTS header added**: `Strict-Transport-Security` is now set for all
  HTTPS connections.
- **Security response headers**: Added `X-Content-Type-Options: nosniff`,
  `X-Frame-Options: DENY`, `Referrer-Policy: same-origin`, and a conservative
  `Content-Security-Policy` (blocks `frame-ancestors`, `object-src`, `base-uri`)
  to harden against clickjacking and base-tag injection.
- **Stored XSS in job message**: `job.metaData.message` is now rendered as
  escaped text (CSS `white-space: pre-wrap`) instead of raw `{@html ...}` in
  `Job.root` and `JobFootprint`.
- **SQL injection in tag queries**: The tag-scope `IN` list and manager project
  subquery in `CountTags` are now parameterized instead of interpolating
  `user.Username` / `user.Projects` values sourced from OIDC/LDAP.
- **GraphQL DoS hardening**: Query cost is bounded with `FixedComplexityLimit`
  (5000). Non-positive `items-per-page` and `page` values are rejected with HTTP
  400 to prevent integer underflow into unbounded `LIMIT`/`OFFSET` queries.
- **CSRF defense-in-depth**: State-changing requests with a cross-site
  `Sec-Fetch-Site` header are now rejected (fails open for non-browser clients),
  complementing the existing `SameSite=Lax` session cookie.
- **NATS API security warning**: A startup warning is now logged when NATS
  subscriptions are enabled, reminding operators that the NATS API has no
  application-layer authentication and that publish ACLs must be restricted at
  the broker.

### Bug fixes

- **Roofline legend placement**: Roofline plot legends now use fixed

+- **JWT HMAC empty-key bypass (critical)**: `jwtSession.go` now refuses to

+  register when `CROSS_LOGIN_JWT_HS512_KEY` is unset. Previously, an empty HMAC

+  key allowed unauthenticated admin token forgery because `golang-jwt` verifies

+  any HS256/HS512 signature against an empty key.

+- **SQL injection via metric names (critical)**: Metric names supplied through

+  GraphQL (`[String!]`) were interpolated raw into `json_extract` SQL expressions.

+  Names are now validated against `^[a-zA-Z0-9_]+$` in

+  `jobsMetricStatisticsHistogram` and `buildFloatJSONCondition`.

+- **Path traversal via line-protocol tags (critical)**: `cluster` and `host`

+  tags from the metric line protocol flowed unvalidated into `path.Join` for

+  checkpoint/WAL file paths, enabling arbitrary file writes outside the

+  checkpoint root via NATS (unauthenticated) or `POST /api/write`. Path-traversal

+  sequences are now rejected in `DecodeLine` before the tags become path

+- **CORS `AllowCredentials` disabled**: CORS middleware no longer sets

+  `AllowCredentials: true`, which was incompatible with `AllowedOrigins: ["*"]`

+  and could enable cross-origin credential theft.

+- **HSTS header added**: `Strict-Transport-Security` is now set for all

+- **Security response headers**: Added `X-Content-Type-Options: nosniff`,

+  `X-Frame-Options: DENY`, `Referrer-Policy: same-origin`, and a conservative

+  `Content-Security-Policy` (blocks `frame-ancestors`, `object-src`, `base-uri`)

+  to harden against clickjacking and base-tag injection.

+- **Stored XSS in job message**: `job.metaData.message` is now rendered as

+  escaped text (CSS `white-space: pre-wrap`) instead of raw `{@html ...}` in

+- **SQL injection in tag queries**: The tag-scope `IN` list and manager project

+  subquery in `CountTags` are now parameterized instead of interpolating

+  `user.Username` / `user.Projects` values sourced from OIDC/LDAP.

+- **GraphQL DoS hardening**: Query cost is bounded with `FixedComplexityLimit`

+  (5000). Non-positive `items-per-page` and `page` values are rejected with HTTP

+  400 to prevent integer underflow into unbounded `LIMIT`/`OFFSET` queries.

+- **CSRF defense-in-depth**: State-changing requests with a cross-site

+  `Sec-Fetch-Site` header are now rejected (fails open for non-browser clients),

+  complementing the existing `SameSite=Lax` session cookie.

+- **NATS API security warning**: A startup warning is now logged when NATS

+  subscriptions are enabled, reminding operators that the NATS API has no

+  application-layer authentication and that publish ACLs must be restricted at


Now update the header to reflect that this also includes security fixes, and check if the README needs any changes:


This is a bugfix release of `cc-backend`, the API backend and frontend
implementation of ClusterCockpit.

This is a security and bugfix release of `cc-backend`, the API backend and
frontend implementation of ClusterCockpit.


This is a bugfix release of `cc-backend`, the API backend and frontend
implementation of ClusterCockpit.

This is a security and bugfix release of `cc-backend`, the API backend and
frontend implementation of ClusterCockpit.

Supports job archive version 3 and database version 11.

-This is a bugfix release of `cc-backend`, the API backend and frontend

+This is a security and bugfix release of `cc-backend`, the API backend and

+frontend implementation of ClusterCockpit.

For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).

If you are upgrading from v1.5.1 no database migration is required.

If you are upgrading from v1.5.0 you need to do another DB migration. This


Now check whether the README needs any updates based on the recent changes (security headers, GraphQL complexity limit, CSRF protection, new startup warning for NATS):


Bash

grep -n "NATS\|security\|CORS\|header" README.md | head -20

Check README for NATS and security references


25:monitoring framework. It provides a REST API and an optional NATS-based messaging
40:[NATS](https://nats.io/) subjects to receive job start/stop events and node
59:on the Go standard library, it is crucial for security and performance to use a
263:    REST API handlers and NATS integration


The README doesn't describe NATS security in detail — that lives in CLAUDE.md (already updated by commit `6f7e262f`). The README overview paragraph is still accurate. No README changes needed.

Let me do a quick sanity-check of the updated ReleaseNotes header:


1	# `cc-backend` version 1.5.4
2
3	Supports job archive version 3 and database version 11.
4
5	This is a security and bugfix release of `cc-backend`, the API backend and
6	frontend implementation of ClusterCockpit.
7	For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).
8	If you are upgrading from v1.5.1 no database migration is required.
9	If you are upgrading from v1.5.0 you need to do another DB migration. This
10	should not take long. For optimal database performance after the migration it is
11	recommended to apply the new `optimize-db` flag, which runs the sqlite `ANALYZE`
12	and `VACUUM` commands. Depending on your database size (more then 40GB) the
13	`VACUUM` may take up to 2h. You can also run the `ANALYZE` command manually.
14	While we are confident that the memory issue with the metricstore cleanup move
15	policy is fixed, it is still recommended to use delete policy for cleanup.
16	This is also the default.
17
18	## Changes in 1.5.4
19
20	### Security fixes
21
22	- **JWT HMAC empty-key bypass (critical)**: `jwtSession.go` now refuses to
23	  register when `CROSS_LOGIN_JWT_HS512_KEY` is unset. Previously, an empty HMAC
24	  key allowed unauthenticated admin token forgery because `golang-jwt` verifies
25	  any HS256/HS512 signature against an empty key.
26	- **SQL injection via metric names (critical)**: Metric names supplied through
27	  GraphQL (`[String!]`) were interpolated raw into `json_extract` SQL expressions.
28	  Names are now validated against `^[a-zA-Z0-9_]+$` in
29	  `jobsMetricStatisticsHistogram` and `buildFloatJSONCondition`.
30	- **Path traversal via line-protocol tags (critical)**: `cluster` and `host`
31	  tags from the metric line protocol flowed unvalidated into `path.Join` for
32	  checkpoint/WAL file paths, enabling arbitrary file writes outside the
33	  checkpoint root via NATS (unauthenticated) or `POST /api/write`. Path-traversal
34	  sequences are now rejected in `DecodeLine` before the tags become path
35	  components.
36	- **CORS `AllowCredentials` disabled**: CORS middleware no longer sets
37	  `AllowCredentials: true`, which was incompatible with `AllowedOrigins: ["*"]`
38	  and could enable cross-origin credential theft.
39	- **HSTS header added**: `Strict-Transport-Security` is now set for all
40	  HTTPS connections.
41	- **Security response headers**: Added `X-Content-Type-Options: nosniff`,
42	  `X-Frame-Options: DENY`, `Referrer-Policy: same-origin`, and a conservative
43	  `Content-Security-Policy` (blocks `frame-ancestors`, `object-src`, `base-uri`)
44	  to harden against clickjacking and base-tag injection.
45	- **Stored XSS in job message**: `job.metaData.message` is now rendered as
46	  escaped text (CSS `white-space: pre-wrap`) instead of raw `{@html ...}` in
47	  `Job.root` and `JobFootprint`.
48	- **SQL injection in tag queries**: The tag-scope `IN` list and manager project
49	  subquery in `CountTags` are now parameterized instead of interpolating
50	  `user.Username` / `user.Projects` values sourced from OIDC/LDAP.
51	- **GraphQL DoS hardening**: Query cost is bounded with `FixedComplexityLimit`
52	  (5000). Non-positive `items-per-page` and `page` values are rejected with HTTP
53	  400 to prevent integer underflow into unbounded `LIMIT`/`OFFSET` queries.
54	- **CSRF defense-in-depth**: State-changing requests with a cross-site
55	  `Sec-Fetch-Site` header are now rejected (fails open for non-browser clients),
56	  complementing the existing `SameSite=Lax` session cookie.
57	- **NATS API security warning**: A startup warning is now logged when NATS
58	  subscriptions are enabled, reminding operators that the NATS API has no
59	  application-layer authentication and that publish ACLs must be restricted at
60	  the broker.
61
62	### Bug fixes
63
64	- **Roofline legend placement**: Roofline plot legends now use fixed
65	  coordinates instead of dynamic placement, preventing the legend from

# `cc-backend` version 1.5.4

Supports job archive version 3 and database version 11.

This is a security and bugfix release of `cc-backend`, the API backend and
frontend implementation of ClusterCockpit.
For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).
If you are upgrading from v1.5.1 no database migration is required.
If you are upgrading from v1.5.0 you need to do another DB migration. This
should not take long. For optimal database performance after the migration it is
recommended to apply the new `optimize-db` flag, which runs the sqlite `ANALYZE`
and `VACUUM` commands. Depending on your database size (more then 40GB) the
`VACUUM` may take up to 2h. You can also run the `ANALYZE` command manually.
While we are confident that the memory issue with the metricstore cleanup move
policy is fixed, it is still recommended to use delete policy for cleanup.
This is also the default.

## Changes in 1.5.4

### Security fixes

- **JWT HMAC empty-key bypass (critical)**: `jwtSession.go` now refuses to
  register when `CROSS_LOGIN_JWT_HS512_KEY` is unset. Previously, an empty HMAC
  key allowed unauthenticated admin token forgery because `golang-jwt` verifies
  any HS256/HS512 signature against an empty key.
- **SQL injection via metric names (critical)**: Metric names supplied through
  GraphQL (`[String!]`) were interpolated raw into `json_extract` SQL expressions.
  Names are now validated against `^[a-zA-Z0-9_]+$` in
  `jobsMetricStatisticsHistogram` and `buildFloatJSONCondition`.
- **Path traversal via line-protocol tags (critical)**: `cluster` and `host`
  tags from the metric line protocol flowed unvalidated into `path.Join` for
  checkpoint/WAL file paths, enabling arbitrary file writes outside the
  checkpoint root via NATS (unauthenticated) or `POST /api/write`. Path-traversal
  sequences are now rejected in `DecodeLine` before the tags become path
  components.
- **CORS `AllowCredentials` disabled**: CORS middleware no longer sets
  `AllowCredentials: true`, which was incompatible with `AllowedOrigins: ["*"]`
  and could enable cross-origin credential theft.
- **HSTS header added**: `Strict-Transport-Security` is now set for all
  HTTPS connections.
- **Security response headers**: Added `X-Content-Type-Options: nosniff`,
  `X-Frame-Options: DENY`, `Referrer-Policy: same-origin`, and a conservative
  `Content-Security-Policy` (blocks `frame-ancestors`, `object-src`, `base-uri`)
  to harden against clickjacking and base-tag injection.
- **Stored XSS in job message**: `job.metaData.message` is now rendered as
  escaped text (CSS `white-space: pre-wrap`) instead of raw `{@html ...}` in
  `Job.root` and `JobFootprint`.
- **SQL injection in tag queries**: The tag-scope `IN` list and manager project
  subquery in `CountTags` are now parameterized instead of interpolating
  `user.Username` / `user.Projects` values sourced from OIDC/LDAP.
- **GraphQL DoS hardening**: Query cost is bounded with `FixedComplexityLimit`
  (5000). Non-positive `items-per-page` and `page` values are rejected with HTTP
  400 to prevent integer underflow into unbounded `LIMIT`/`OFFSET` queries.
- **CSRF defense-in-depth**: State-changing requests with a cross-site
  `Sec-Fetch-Site` header are now rejected (fails open for non-browser clients),
  complementing the existing `SameSite=Lax` session cookie.
- **NATS API security warning**: A startup warning is now logged when NATS
  subscriptions are enabled, reminding operators that the NATS API has no
  application-layer authentication and that publish ACLs must be restricted at
  the broker.

### Bug fixes

- **Roofline legend placement**: Roofline plot legends now use fixed
  coordinates instead of dynamic placement, preventing the legend from


Looks good. The README is accurate as-is — the recent changes are all backend security hardening with no new user-facing configuration options that would require README updates.

**Summary:** `ReleaseNotes.md` now has a **Security fixes** section at the top of the 1.5.4 changes documenting all 10 issues fixed across the three security audit commits: three critical fixes (JWT empty-key bypass, SQL injection via metric names, path traversal via line-protocol tags), plus medium-severity fixes (CORS, HSTS, security headers, stored XSS, tag SQL injection, GraphQL DoS, CSRF). The release description is updated from "bugfix" to "security and bugfix release". The README needed no changes.


<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/exit</command-name>
            <command-message>exit</command-message>
            <command-args></command-args>


<local-command-stdout>Bye!</local-command-stdout>