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

high

How trailofbits.python.pickles-in-pytorch.pickles-in-pytorch happens in Python/PyTorch and how to fix it

A high-severity deserialization vulnerability was fixed in `skills/packs/pipeline-phase-5-pretrain-code/scripts/trainer.py` where `torch.save()` was used to serialize model checkpoints. Because PyTorch's save mechanism relies on Python's `pickle` module internally, any checkpoint file loaded later could execute arbitrary code. The fix replaces `torch.save()` with `np.savez()` for model weights and a JSON file for metadata, eliminating the pickle-based serialization entirely.

high

How Denial of Service via Unbounded Brace Expansion Happens in Node.js Dependencies and How to Fix It

A critical vulnerability in adm-zip (CVE-2026-39244) allowed attackers to craft malicious ZIP files that trigger unbounded brace expansion, causing excessive memory allocation and process crashes. The CortexKit project fixed this by upgrading adm-zip from 0.5.17 to 0.6.0, which implements bounds checking on expansion operations. This vulnerability demonstrates why dependency management and timely security updates are essential for production Node.js applications.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How unsafe token deserialization happens in Node.js Temml parser and how to fix it

A critical vulnerability in the Temml math library's parser allowed unsafe token deserialization that could lead to remote code execution when processing user-supplied mathematical expressions. The fix adds strict type validation on fetched token properties before use, preventing exploitation of malformed or crafted payloads.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period on dependency updates, meaning Dependabot could immediately propose upgrades to newly published — potentially malicious or unstable — package versions. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, introducing a mandatory waiting period before any newly released version is surfaced as an update candidate. Because this project