Operator session tokens are stored in plaintext in the operator_sessions table (the token column is the PRIMARY KEY). The session token is a 32-byte random hex value sent directly in a cookie and valid for 24 hours.
internal/models/operator.go:61 — OperatorSession.Token holds the plaintext token.internal/store/sqlite_operators.go:590 — CreateOperatorSession inserts sess.Token verbatim.internal/store/sqlite_operators.go:603,642,681,698 — lookups/updates/deletes use WHERE token = ? against the plaintext value.Anyone who can read the database (backup, snapshot, file copy, or SQL-level disclosure) obtains every active session token and can hijack operator sessions directly, with no further authentication.
This is functionally identical to the plaintext enrollment-token issue fixed in GHSA-ghmh-jhmj-wcmf. API keys (OperatorAPIKey.KeyHash) and enrollment tokens (EnrollmentToken.TokenHash) already store only a SHA256 hash; session tokens were missed.
Store only a SHA256 hash of the session token, mirroring API keys and enrollment tokens:
HashSessionToken helper (alongside the existing token-hash helpers).token_hash column.CreateOperatorSession, PromoteOperatorSession, and GetOperatorBySession to write/look up by hash.token column in a follow-up migration.Sessions are ephemeral (24h TTL), so all active sessions can be invalidated on deployment — no backward compatibility needed.
Restrict and encrypt database backups; rotate the operator database. These mitigate exposure but do not fix the underlying storage of plaintext tokens.
internal/models/operator.go:58-66internal/store/sqlite_operators.go:577-698005_operators.up.sql:27{
"cwe_ids": [
"CWE-312",
"CWE-522"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-14T20:17:04Z",
"nvd_published_at": null,
"severity": "HIGH"
}