Back to Blog
high SEVERITY8 min read

How Unsafe Deserialization into interface{} happens in Go and how to fix it

A high-severity unsafe deserialization vulnerability was discovered in `web/session/session.go` where a type assertion on an `interface{}` value was performed without checking success, enabling arbitrary data structures to flow into the application. The fix adds a two-branch type assertion that returns `nil` when the cast fails, preventing unexpected types from propagating. This pattern is common in Go session management code and is easy to overlook during code review.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

This is an unsafe deserialization vulnerability (CWE-502) in Go, found in `web/session/session.go`. The `GetLoginUser` function retrieved a session value typed as `interface{}` and cast it directly to `model.User` without verifying the assertion succeeded, allowing arbitrary or malformed data to be treated as a valid user object. The fix replaces the bare type assertion `obj.(model.User)` with the two-value form `user, ok := obj.(model.User)`, returning `nil` when the assertion fails, so only a genuine `model.User` value can proceed.

Vulnerability at a Glance

cweCWE-502
fixReplace with two-value assertion `user, ok := obj.(model.User)` and return nil on failure
riskArbitrary or attacker-influenced data structures treated as authenticated user objects
languageGo
root causeBare type assertion `obj.(model.User)` panics on unexpected types and skips validation
vulnerabilityUnsafe deserialization via unchecked interface{} type assertion

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:

  1. No type validation. If obj is not a model.User, the assertion panics, crashing the goroutine handling the request. An attacker who can force a non-model.User value into the session can trigger a denial-of-service.

  2. No rejection path. Even if the panic is recovered somewhere up the call stack, control flow skips the return nil path, so the caller may receive a zero-value user or behave unpredictably.

  3. Composability with upstream flaws. If any other code path calls c.Set(ctxKeyUser, <attacker-controlled-value>), this function will attempt to cast that value to model.User. Combined with a session fixation or an insecure deserialization in the session store, an attacker could inject a crafted model.User-shaped object with elevated privileges (e.g., IsAdmin: true).

Attack scenario specific to this code

Consider the following chain:

  1. The session backend deserializes session data from a cookie or Redis using a generic decoder that produces interface{} values.
  2. An attacker crafts a session payload that, when decoded, places a map[string]interface{} (instead of a model.User struct) under the ctxKeyUser key.
  3. GetLoginUser is called by checkLogin middleware for every route under /server.
  4. The bare assertion obj.(model.User) panics because map[string]interface{} is not model.User.
  5. If the panic is recovered and the request continues, checkLogin may 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 obj holds.
  • Explicit rejection. The function returns nil for any non-model.User value, and the caller (checkLogin middleware) treats a nil user as unauthenticated — the safe default.
  • No behaviour change for valid sessions. When the session store correctly contains a model.User, ok is true and 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.


Key Takeaways

  • Bare type assertions on interface{} session values are a latent panic bomb. In GetLoginUser, the pattern obj.(model.User) would crash any goroutine that encountered a non-model.User session value, enabling remote denial-of-service against all /server routes.
  • The ok idiom 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. GetLoginUser is called by checkLogin middleware on every protected route; a flaw here has application-wide authentication impact.
  • A two-line fix is sometimes all it takes. Adding ok and an if !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.User values early in GetLoginUser, 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) in web/session/session.go, typed as interface{} — a value whose concrete type cannot be statically guaranteed.
  • Sink: Bare type assertion obj.(model.User) at line 31 of web/session/session.go, which panics on type mismatch and skips all validation.
  • Missing control: No ok check 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 form user, ok := obj.(model.User) and added if !ok { return nil } to reject non-model.User values 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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #402

Related Articles

high

js-yaml 5.2.1 DoS: Exponential Parsing in Flow Collections

A denial-of-service vulnerability in js-yaml 5.2.1 allows an attacker to crash the parser by supplying deeply nested flow collections that trigger exponential parsing behavior. The fix in version 5.2.2 improves the parser's handling of these structures, preventing the algorithmic complexity attack. This is critical for any service accepting user-controlled YAML input.

high

Anthropic API Adapter Prototype Pollution in parseToolCallInput

The `parseToolCallInput` function in the Anthropic API adapter used `JSON.parse` without protecting against prototype pollution keys. An attacker who could manipulate API responses—through a man-in-the-middle attack, compromised Codex backend, or DNS spoofing—could inject `__proto__`, `constructor`, or `prototype` properties to pollute JavaScript's Object prototype and affect extension runtime behavior.

high

load_localStorage.js JSON Parse: Prototype Pollution via __proto__

The load_localStorage.js utility parsed JSON configuration without validating keys, permitting prototype pollution through malicious `__proto__`, `constructor`, or `prototype` properties. An attacker with filesystem access could poison downstream JavaScript execution by injecting these special keys into the loaded data structure.

critical

How Arbitrary Code Execution Happens in protobufjs and How to Fix It

CVE-2026-41242 is a critical vulnerability in protobufjs versions 8.0.0 and earlier that allows attackers to execute arbitrary code by injecting malicious type fields into protobuf definitions. The fix upgrades the dependency from `^8.0.0` to `^8.6.6` in `core/package.json`, eliminating the unsafe code path that processed attacker-controlled type metadata without validation.

critical

How Type Confusion Vulnerabilities Happen in JavaScript Dependencies and How to Fix Them

A critical type confusion vulnerability (CVE-2021-23436) was discovered in immer 9.0.7, a popular immutable state management library used in the client application. By upgrading to immer 9.0.6, the vulnerability was patched, eliminating a flaw that could have allowed attackers to bypass previous security fixes (CVE-2020-28477). This fix demonstrates why keeping dependencies current is essential for maintaining application security.

critical

How Prototype Pollution Happens in i18next-fs-backend and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-48713) was discovered in i18next-fs-backend versions prior to 2.6.6, where specially crafted missing-key strings could pollute the JavaScript object prototype. This fix upgrades the dependency to patch the vulnerability and prevent attackers from injecting malicious properties into application objects.