Introduction
The web/session/session.go file is responsible for one of the most security-sensitive tasks in any web application: identifying who the current user is. The GetLoginUser function reads a value from the Gin context, which acts as a key-value store populated by session middleware. Because Gin's context stores values as interface{}, any concrete type can be placed there — including types that are not model.User.
Before this fix, line 31 of session.go contained:
user := obj.(model.User)
This single-line bare type assertion is the vulnerability. If obj holds anything other than a model.User, Go panics at runtime. More subtly, if session storage can be influenced by an attacker — through session fixation, cookie tampering, or a chained deserialization flaw upstream — the application has no gate to reject a value of the wrong type before treating it as a trusted user object.
This post explains exactly why that pattern is dangerous in a Go HTTP service, what an attacker could do with it, and how the two-line fix closes the door.
The Vulnerability Explained
What interface{} deserialization means in Go
In Go, interface{} (or its modern alias any) is a type that can hold a value of any concrete type. When you retrieve a value from a generic store — a cache, a session map, a JSON blob decoded without a schema — you get back an interface{}. To use it as a specific type, you must assert the type.
Go offers two forms of type assertion:
// Bare assertion — panics if obj is not model.User
user := obj.(model.User)
// Safe assertion — sets ok=false instead of panicking
user, ok := obj.(model.User)
The bare form is fine when you are certain of the type. In session management code, you are never certain — the session value originates from outside the current function's control.
The vulnerable code (before the fix)
// web/session/session.go — BEFORE
func GetLoginUser(c *gin.Context) *model.User {
obj, _ := c.Get(ctxKeyUser)
if obj == nil {
return nil
}
user := obj.(model.User) // ← bare assertion, no ok check
return &user
}
Three things make this dangerous:
-
No type validation. If
objis not amodel.User, the assertion panics, crashing the goroutine handling the request. An attacker who can force a non-model.Uservalue into the session can trigger a denial-of-service. -
No rejection path. Even if the panic is recovered somewhere up the call stack, control flow skips the
return nilpath, so the caller may receive a zero-value user or behave unpredictably. -
Composability with upstream flaws. If any other code path calls
c.Set(ctxKeyUser, <attacker-controlled-value>), this function will attempt to cast that value tomodel.User. Combined with a session fixation or an insecure deserialization in the session store, an attacker could inject a craftedmodel.User-shaped object with elevated privileges (e.g.,IsAdmin: true).
Attack scenario specific to this code
Consider the following chain:
- The session backend deserializes session data from a cookie or Redis using a generic decoder that produces
interface{}values. - An attacker crafts a session payload that, when decoded, places a
map[string]interface{}(instead of amodel.Userstruct) under thectxKeyUserkey. GetLoginUseris called bycheckLoginmiddleware for every route under/server.- The bare assertion
obj.(model.User)panics becausemap[string]interface{}is notmodel.User. - If the panic is recovered and the request continues,
checkLoginmay incorrectly evaluate the authentication state, potentially allowing unauthenticated access.
Even without a full exploit, step 4 alone is a remotely-triggerable panic — a denial-of-service primitive against every authenticated endpoint in the service.
The Fix
What changed
The fix is in web/session/session.go and modifies exactly the type assertion on line 31:
Before:
user := obj.(model.User)
return &user
After:
user, ok := obj.(model.User)
if !ok {
return nil
}
return &user
Why this specific change is sufficient
The two-value assertion user, ok := obj.(model.User) never panics. When obj holds a value that is not model.User, Go sets ok to false and user to the zero value of model.User. The added if !ok { return nil } block ensures that only a genuine model.User value can pass through to the caller.
This means:
- Panic eliminated. No runtime crash regardless of what type
objholds. - Explicit rejection. The function returns
nilfor any non-model.Uservalue, and the caller (checkLoginmiddleware) treats aniluser as unauthenticated — the safe default. - No behaviour change for valid sessions. When the session store correctly contains a
model.User,okistrueand the function behaves identically to before.
Full diff
- user := obj.(model.User)
+ user, ok := obj.(model.User)
+ if !ok {
+ return nil
+ }
return &user
Two lines added, one line changed — a minimal, surgical fix with zero risk of breaking valid authentication flows.
Prevention & Best Practices
1. Always use the two-value type assertion in session/context code
Any time you retrieve a value from gin.Context, context.Context, a cache, or any interface{}-typed store, use:
value, ok := iface.(YourConcreteType)
if !ok {
// handle gracefully
}
Make this a team coding standard enforced by linter rules.
2. Prefer typed session wrappers
Instead of storing raw model.User values in the Gin context and retrieving them as interface{}, wrap the get/set operations in typed helper functions that encapsulate the assertion:
// SetLoginUser stores a user with a known key
func SetLoginUser(c *gin.Context, user model.User) {
c.Set(ctxKeyUser, user)
}
// GetLoginUser retrieves and type-asserts safely
func GetLoginUser(c *gin.Context) *model.User {
obj, exists := c.Get(ctxKeyUser)
if !exists || obj == nil {
return nil
}
user, ok := obj.(model.User)
if !ok {
return nil
}
return &user
}
This pattern confines the assertion to one place, making it easy to audit.
3. Validate session store integrity
If your session backend (Redis, a signed cookie, a database) deserializes session data using a generic decoder (e.g., encoding/gob, encoding/json into interface{}), ensure the deserialized value is validated against a schema or concrete struct before it is placed into the Gin context.
4. Use go-safeinput/safedeserialize
The github.com/ravisastryk/go-safeinput/safedeserialize package provides automatic protection for deserialization paths. It rejects inputs that do not match the expected schema, removing the burden of manual type checking.
5. Enable static analysis in CI
Add Semgrep with the go-unsafe-deserialization-interface rule to your CI pipeline:
- name: Semgrep scan
run: semgrep --config "p/golang" --error
This rule flags bare type assertions on interface{} values, catching this class of bug before it reaches production.
6. Reference standards
- CWE-502: Deserialization of Untrusted Data — the root CWE for this vulnerability class.
- OWASP A08:2021 – Software and Data Integrity Failures — covers insecure deserialization in the OWASP Top 10.
Key Takeaways
- Bare type assertions on
interface{}session values are a latent panic bomb. InGetLoginUser, the patternobj.(model.User)would crash any goroutine that encountered a non-model.Usersession value, enabling remote denial-of-service against all/serverroutes. - The
okidiom is not optional in untrusted contexts. Go's two-value type assertion is the language's built-in mechanism for safe deserialization — skipping it in session code is equivalent to skipping a bounds check. - Session management functions are high-value targets.
GetLoginUseris called bycheckLoginmiddleware on every protected route; a flaw here has application-wide authentication impact. - A two-line fix is sometimes all it takes. Adding
okand anif !ok { return nil }block completely eliminates the vulnerability without touching any other logic. - Type safety at the session boundary prevents privilege escalation chains. By rejecting non-
model.Uservalues early inGetLoginUser, the fix breaks any chain that attempts to inject a crafted user object with elevated privileges through the session store.
How Orbis AppSec Detected This
- Source: Session value retrieved via
c.Get(ctxKeyUser)inweb/session/session.go, typed asinterface{}— a value whose concrete type cannot be statically guaranteed. - Sink: Bare type assertion
obj.(model.User)at line 31 ofweb/session/session.go, which panics on type mismatch and skips all validation. - Missing control: No
okcheck on the type assertion; no fallback path for unexpected types; no schema validation of the session value before assertion. - CWE: CWE-502 — Deserialization of Untrusted Data.
- Fix: Replaced
user := obj.(model.User)with the two-value formuser, ok := obj.(model.User)and addedif !ok { return nil }to reject non-model.Uservalues before they propagate.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The GetLoginUser function in web/session/session.go is the single gateway through which every authenticated request in this Go service obtains its user identity. A bare type assertion on an interface{} value at that gateway — without checking whether the assertion succeeded — created a vulnerability that was simultaneously a runtime panic risk and a potential privilege escalation primitive.
The fix is elegant in its simplicity: two lines that add the ok idiom Go was designed to use for exactly this purpose. It costs nothing in performance, changes nothing for valid sessions, and eliminates an entire class of type-confusion attack at the most sensitive point in the authentication flow.
If you maintain Go services that use Gin (or any framework that stores context values as interface{}), audit every type assertion in your session and middleware code today. The pattern is pervasive, the fix is trivial, and the consequences of leaving it unfixed are not.