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
Published August 26, 2026Reviewed 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.


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. 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.


References

Frequently Asked Questions

What is unsafe deserialization in Go?

Unsafe deserialization in Go occurs when data retrieved from an untrusted or loosely-typed source (such as an `interface{}` session value) is cast to a concrete type without verifying the cast succeeds, allowing unexpected or malicious data structures to enter application logic.

How do you prevent unsafe deserialization in Go?

Always use the two-value type assertion form (`value, ok := iface.(ConcreteType)`) and handle the failure case explicitly. Prefer storing concrete types in session stores rather than `interface{}` wherever possible.

What CWE is unsafe deserialization?

Unsafe deserialization maps to CWE-502: Deserialization of Untrusted Data.

Is checking for nil on an interface{} enough to prevent unsafe deserialization in Go?

No. A nil check only confirms the interface holds a value; it does not verify the underlying concrete type is what you expect. You must use a type assertion with the `ok` idiom to confirm the type before using the value.

Can static analysis detect unsafe deserialization in Go?

Yes. Tools such as Semgrep (with the `go-unsafe-deserialization-interface` rule), gosec, and CodeQL can flag bare type assertions on `interface{}` values that originate from external or session data.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #402

Related Articles

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.

high

How Quadratic CPU Consumption Happens in JS-YAML and How to Fix It

A critical vulnerability in JS-YAML versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through maliciously crafted YAML input using the `!!omap` tag resolver. The vulnerability stems from inefficient array operations in the ordered map resolution logic, which could be exploited for denial-of-service attacks. Upgrading to JS-YAML 4.3.1 or 3.15.1 patches this attack surface by optimizing the computational complexity of ordered map processing.

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.

high

How Quadratic CPU Consumption in js-yaml's !!omap Resolution Happens in Node.js and How to Fix It

A high-severity algorithmic complexity vulnerability (GHSA-5p4m-2wfm-xmqj) in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through crafted YAML input using the `!!omap` tag. The fix upgrades js-yaml from 4.1.1 to 4.3.1 in the Audex desktop music player, eliminating a denial-of-service vector that could freeze the Electron application when parsing untrusted YAML content.

critical

How Prototype Pollution Happens in JavaScript Carousel Libraries and How to Fix It

A critical prototype pollution vulnerability (CVE-2026-27212) was discovered in Swiper versions up to 11.2.10, a popular JavaScript carousel library used in production web applications. This vulnerability could allow attackers to manipulate application behavior through the prototype chain. The fix involved upgrading Swiper from 11.2.10 to 12.1.2, which patches the underlying prototype pollution flaw.