Native object serialization can be convenient inside a trusted process boundary. A runtime can preserve object types, references, inheritance details, and other implementation state with very little application code.

That convenience becomes dangerous when serialized bytes cross a trust boundary.

Many native object formats do more than decode passive data. Their decoders may resolve classes, allocate arbitrary object graphs, invoke constructors or callbacks, restore proxies, or trigger other runtime behavior. An attacker who controls the input can then influence operations that were never intended to be part of parsing.

The safest default is simple: do not feed untrusted bytes to a native object deserializer. Parse an explicit data format into a small data model, validate it, and only then construct application objects.

Data decoding and object reconstruction are different jobs

A JSON document such as this describes values:

{
  "customer_id": "c_2048",
  "quantity": 3,
  "priority": false
}

A strict decoder can map those fields into strings, integers, and booleans. The application still decides what those values mean and which domain objects may be created.

Native object serialization often carries richer instructions. Depending on the runtime and format, a stream may identify concrete classes, encode private fields, restore references between objects, or participate in hooks used during reconstruction.

That extra capability expands the parser’s authority.

A useful security boundary is:

external bytes
    |
    v
passive data parser
    |
    v
validated data structure
    |
    v
explicit application logic

The parser should not get permission to select executable types merely because a byte stream names them.

Treat type selection as authority

Suppose an application expects a serialized Cart object. It may appear sufficient to check that the final object is a cart after deserialization.

That check can happen too late.

A decoder may need to instantiate nested objects before it can return the top-level value. If a nested type has a dangerous reconstruction hook, side effects can occur during decoding. Rejecting the final result does not undo those effects.

The security decision therefore belongs before object construction, not after it.

This principle also applies to formats with polymorphic type metadata. Features that accept a type name from input and dynamically resolve it into a runtime class can turn a normal data decoder into an object-construction engine.

Keep type mappings fixed in application code.

Prefer explicit wire models

For data that arrives from browsers, mobile clients, message queues, partner systems, uploaded files, or other untrusted sources, use a format designed to represent data rather than arbitrary runtime state.

Common choices include JSON, Protocol Buffers, CBOR, and MessagePack. The format alone is not a complete defense; decoder configuration still matters. The important property is that the application owns the schema and controls conversion into domain objects.

For example, an API can accept a narrow request model:

type CreateOrderRequest struct {
    ProductID string `json:"product_id"`
    Quantity  int    `json:"quantity"`
}

After decoding, validate every security-relevant constraint:

if req.ProductID == "" {
    return errors.New("product_id is required")
}
if req.Quantity < 1 || req.Quantity > 100 {
    return errors.New("quantity is outside the accepted range")
}

Only validated values proceed to business logic. The client does not get to nominate a Go type, database model, service object, or callback.

This separation also makes authorization easier to review. A transport structure contains client-supplied facts; server-owned fields such as account role, approval status, internal price, or tenant identity come from trusted server state.

Do not replace one risky decoder with another

Moving from native serialization to JSON is useful only when the JSON decoder remains a data decoder.

Avoid configurations that consume attacker-controlled class names or type identifiers and automatically instantiate matching runtime classes. A field such as "type": "express" is safe when application code maps it to a small, fixed enum. It is risky when a framework interprets the value as a class name.

Prefer an explicit mapping:

"standard" -> StandardShipmentData
"express"  -> ExpressShipmentData

Reject every other value.

Do not resolve arbitrary classes through reflection, package names, module names, assembly names, or similar runtime identifiers supplied by the request.

Apply strict schema rules

A narrow schema reduces ambiguity and limits the amount of parser behavior exposed to an attacker.

Define:

  • accepted fields;
  • required fields;
  • field types;
  • numeric ranges;
  • maximum string and collection sizes;
  • accepted enum values;
  • nesting limits where the parser supports them;
  • handling for duplicate or unknown fields.

Reject malformed input early.

Unknown fields deserve an explicit policy. Ignoring them can help compatibility, but strict rejection is often better for security-sensitive commands because misspelled or unexpected fields cannot silently pass through. The correct choice depends on the protocol contract, but it should be deliberate.

Duplicate keys also need attention. Different components may interpret duplicate object members differently. If a gateway uses the first value and an application uses the last, security checks can disagree with execution. Reject duplicate keys when the chosen parser permits that policy.

Bound parser resource use

A passive format can still be abused for denial of service.

Set limits before parsing:

maximum request bytes
maximum decoded message size
maximum collection length
maximum nesting depth
maximum string length
processing deadline

Do not rely on a reverse proxy as the only size control. Internal consumers, background jobs, tests, or alternate ingress paths may bypass that proxy.

Compressed input needs separate care. A small compressed payload can expand into a much larger decoded stream. Apply limits to decompressed data as well as the transport body.

For message brokers, enforce limits at both the producer-facing boundary and the consumer. A consumer should remain safe even if another trusted component publishes a malformed or oversized message by mistake.

Legacy native formats need containment

Some systems cannot remove native serialization immediately. A migration may need to process historical records or communicate with an older component.

Treat that path as a temporary high-risk boundary.

First, stop creating new data in the legacy format where possible. Write new records using the replacement format while retaining a controlled reader for old records.

Next, narrow the source. A legacy decoder should not accept arbitrary network requests, cookies, query parameters, uploaded blobs, or message fields. Restrict it to data from a specifically controlled store or migration channel.

If the platform provides an allowlist mechanism for permitted types, use the smallest possible set. An allowlist can reduce exposure, but it is not equivalent to a passive parser. Allowed classes can gain dangerous behavior after dependency or application changes.

For especially sensitive migrations, isolate the decoder in a separate process with minimal filesystem, network, credential, and operating-system privileges. Return only a simple validated data representation to the main application.

Containment reduces impact if the legacy decoder behaves unexpectedly.

Integrity protection does not make hostile object graphs safe

A message authentication code or digital signature can establish that bytes came from a holder of a key. It does not automatically establish that the serialized object graph is safe to reconstruct.

Signatures are valuable when every signer is trusted to choose the complete object graph and key handling is strong. They are not a general substitute for removing dangerous deserialization.

This distinction matters in distributed systems. A service may correctly authenticate a message from another service while still receiving data influenced by an external user. Trust in the sending service’s identity does not imply that every nested value is safe for native object reconstruction.

Validate data according to its original trust source.

Keep caches and sessions data-oriented

Serialized objects often appear in server-side sessions, distributed caches, and job queues because native serialization makes persistence easy.

These locations can drift into security boundaries over time.

A cache entry may become writable through another service. A queue may gain a new producer. Session data may be copied between environments. A database import tool may restore records from a less trusted source.

Store compact data structures instead of executable object graphs:

{
  "account_id": "a_731",
  "cart_id": "cart_88",
  "issued_at": 1789156800
}

On read, validate the structure and fetch authoritative state from trusted storage. Avoid persisting privileged domain objects whose internal fields could bypass current authorization rules when restored.

Data-oriented storage also makes version migrations more predictable because schema changes are explicit.

Review framework features that hide deserialization

Dangerous reconstruction is not always visible as a direct call named deserialize.

Inspect features that:

  • restore objects from cookies or session blobs;
  • decode background-job arguments;
  • hydrate cache entries into arbitrary classes;
  • accept polymorphic request models;
  • import application state;
  • process RPC envelopes with dynamic types;
  • restore objects from database columns.

Search dependency documentation and configuration for native serialization, object streams, type metadata, polymorphic binding, and reconstruction hooks.

The key review question is: can bytes influenced by an untrusted party cause the runtime to choose or instantiate a type?

If the answer is yes, redesign the boundary toward passive data.

Test the boundary, not only valid examples

Security tests should confirm rejection behavior.

Include cases with:

  • unknown fields;
  • unsupported type discriminators;
  • deeply nested structures;
  • oversized arrays and strings;
  • duplicate keys;
  • malformed encodings;
  • truncated messages;
  • unexpected numeric forms;
  • legacy serialized bytes presented to the new endpoint.

A good test suite proves that the decoder accepts the documented protocol and rejects everything outside it without invoking application behavior.

Fuzz testing is also useful for parsers and conversion layers. Give the fuzzer a bounded input size and assert that parsing cannot panic, hang, exceed intended resource limits, or construct types outside the approved data model.

Migration checklist

When replacing native object deserialization at a trust boundary:

  1. Identify every source of serialized bytes and classify its trust level.
  2. Choose an explicit data format with a documented schema.
  3. Create transport models containing only client-controlled fields.
  4. Configure strict parsing and resource limits.
  5. Validate values before constructing domain objects.
  6. Keep type mappings fixed and reject unknown discriminators.
  7. Move server-owned state out of incoming payloads.
  8. Stop writing new records in the legacy format.
  9. Isolate any required legacy reader and minimize its privileges.
  10. Add rejection, limit, and fuzz tests.
  11. Remove the legacy reader after the final old record or peer is gone.

A narrow decoder creates a stronger boundary

Serialization is not merely a storage detail when data crosses a trust boundary. A decoder defines what an input is allowed to cause.

Native object reconstruction often grants more authority than an external message needs. It can couple wire data to runtime classes and make object creation part of parsing.

A stronger design keeps the boundary small: decode passive values, enforce a strict schema, validate limits and semantics, then construct application state through explicit code.

That structure gives reviewers a clear place to inspect trust decisions and gives attackers far less control over runtime behavior.