Skip to content

Security Considerations

Glaze is designed with security in mind, particularly for parsing untrusted data from network sources. This document describes the security measures in place and best practices for handling potentially malicious input.

Binary Format Security (BEVE, MessagePack, CBOR)

Memory Bomb (DoS) Protection

Binary formats like BEVE encode length headers that indicate how many elements or bytes follow. A malicious actor could craft a message with a huge length header (e.g., claiming 1 billion elements) but minimal actual data. Without proper protection, this could cause:

  • Memory exhaustion: The parser calls resize() with the malicious size, consuming all available memory
  • Process termination: The system's OOM killer terminates the process
  • Denial of Service: The server becomes unresponsive

How Glaze Protects Against This

Glaze validates length headers against the remaining buffer size before any memory allocation. If a length header claims more data than exists in the buffer, parsing fails immediately with error_code::invalid_length.

// Example: Malicious buffer claiming 1 billion strings
std::vector<std::string> result;
auto ec = glz::read_beve(result, malicious_buffer);
// ec.ec == glz::error_code::invalid_length
// No memory was allocated for the claimed 1 billion strings

This protection applies to:

  • Strings: Length must not exceed remaining buffer bytes
  • Typed arrays (numeric, boolean, string, complex): Element count validated against buffer size
  • Generic arrays: Element count validated against buffer size
  • Maps/Objects: Entry count validated against buffer size

Protection Guarantees

Data Type Validation
std::string Length ≤ remaining buffer bytes
std::vector<T> (numeric) Count × sizeof(T) ≤ remaining buffer bytes
std::vector<bool> (Count + 7) / 8 ≤ remaining buffer bytes
std::vector<std::string> Count ≤ remaining buffer bytes (minimum 1 byte per string header)
Generic arrays Count ≤ remaining buffer bytes (minimum 1 byte per element)

32-bit System Considerations

On 32-bit systems, BEVE length headers using 8-byte encoding (for values > 2^30) are rejected with invalid_length since these values cannot be addressed in 32-bit memory space.

User-Configurable Allocation Limits

For applications that need stricter control over memory allocation, Glaze provides compile-time options to limit the maximum size of strings and arrays during parsing.

Global Limits via Custom Options

Apply limits to all strings/arrays in a parse operation:

// Define custom options with allocation limits
struct secure_opts : glz::opts
{
   uint32_t format = glz::BEVE;
   size_t max_string_length = 1024;    // Max 1KB per string
   size_t max_array_size = 10000;      // Max 10,000 elements per array
};

std::vector<std::string> data;
auto ec = glz::read<secure_opts{}>(data, buffer);
if (ec.ec == glz::error_code::invalid_length) {
    // A string or array exceeded the configured limit
}

Per-Field Limits via Wrapper

Apply limits to specific fields using glz::max_length:

struct UserInput
{
   std::string username;       // Should be limited
   std::string bio;            // Can be longer
   std::vector<int> scores;    // Should be limited
};

template <>
struct glz::meta<UserInput>
{
   using T = UserInput;
   static constexpr auto value = object(
      "username", glz::max_length<&T::username, 64>,   // Max 64 chars
      "bio", &T::bio,                                   // No limit
      "scores", glz::max_length<&T::scores, 100>       // Max 100 elements
   );
};

These options work together with buffer-based validation:

  1. Buffer validation: Rejects claims that exceed buffer capacity (always enabled)
  2. Allocation limits: Rejects allocations that exceed user-defined limits (optional)

This allows you to accept legitimately large data while protecting against excessive memory usage.

Runtime Limits via Custom Context

For applications that need to set limits dynamically at runtime (e.g., based on user permissions or request type), inherit from glz::context and add constraint fields:

struct secure_context : glz::context
{
   size_t max_string_length = 0;  // 0 = no limit
   size_t max_array_size = 0;
   size_t max_map_size = 0;
};

// Set limits dynamically based on user tier
secure_context ctx;
if (user.is_premium()) {
   ctx.max_array_size = 100000;
} else {
   ctx.max_array_size = 1000;
}

std::vector<Item> items;
auto ec = glz::read<glz::opts{.format = glz::BEVE}>(items, buffer, ctx);
if (ec.ec == glz::error_code::invalid_length) {
   // Limit exceeded
}

Runtime constraints use if constexpr to detect the presence of constraint fields, so there is zero binary overhead when using the base glz::context.

When to use each approach:

Approach Use Case
Compile-time (opts) Fixed schema limits, per-field limits via glz::meta
Runtime (context) Dynamic limits based on user, request type, or configuration

Both mechanisms can be used together—either can reject a value that exceeds its limit.

Best Practices for Network Applications

  1. Limit input buffer size: Control the maximum message size your application accepts at the network layer, before passing data to Glaze.
constexpr size_t MAX_MESSAGE_SIZE = 1024 * 1024; // 1 MB limit

void handle_message(const std::span<const std::byte> data) {
    if (data.size() > MAX_MESSAGE_SIZE) {
        // Reject oversized messages before parsing
        return;
    }

    MyStruct obj;
    auto ec = glz::read_beve(obj, data);
    if (ec) {
        // Handle parse error
    }
}
  1. Use appropriate container types: Consider using fixed-size containers like std::array when the maximum size is known at compile time.

  2. Validate after parsing: Use glz::read_constraint for business logic validation after successful parsing.

template <>
struct glz::meta<UserInput> {
    using T = UserInput;
    static constexpr auto value = object(
        "username", glz::read_constraint<&T::username, [](const auto& s) {
            return s.size() <= 64;
        }>
    );
};

Raw Pointer Allocation Safety

By default, Glaze refuses to allocate memory for null raw pointers during deserialization. This prevents a class of memory leaks where the parser would call new without any mechanism to ensure the memory is freed.

struct example { int x, y, z; };

std::vector<example*> vec;
auto ec = glz::read_beve(vec, buffer);
// ec.ec == glz::error_code::invalid_nullable_read (safe default)

If your application requires raw pointer allocation, you can enable it with allocate_raw_pointers = true:

struct alloc_opts : glz::opts {
   bool allocate_raw_pointers = true;
};

std::vector<example*> vec;
auto ec = glz::read<alloc_opts{}>(vec, buffer);
// Works, but you MUST manually delete allocated pointers
for (auto* p : vec) delete p;

Runtime Raw Pointer Allocation Control

For applications that need to toggle raw pointer allocation at runtime (e.g., based on data source trustworthiness), use a custom context:

struct secure_context : glz::context
{
   bool allocate_raw_pointers = false;
};

template <typename T>
T my_deserializer(const std::vector<std::byte>& buffer, bool is_trusted_source)
{
   T obj{};
   secure_context ctx;
   ctx.allocate_raw_pointers = is_trusted_source;

   auto ec = glz::read<glz::opts{.format = glz::BEVE}>(obj, buffer, ctx);
   if (ec) {
      // Handle error
   }
   return obj;
}

This allows building unified deserializers that scale security based on the trust level of the source—for example, allowing raw pointer allocation for local save files while disabling it for network packets.

[!WARNING] When enabling allocate_raw_pointers, your application is responsible for tracking and freeing all allocated memory. Consider using smart pointers (std::unique_ptr, std::shared_ptr) instead, which Glaze handles automatically and safely.

JSON Format Security

Safe Parsing Defaults

  • No buffer overruns: All parsing operations validate bounds before accessing data
  • No null pointer dereferences: Null checks are performed where applicable
  • Integer overflow protection: Numeric parsing handles overflow conditions

Untrusted Input

When parsing JSON from untrusted sources:

  1. Limit recursion depth: Deeply nested structures can cause stack overflow. Consider flattening data structures or implementing depth limits at the application level.

  2. Limit string sizes: Very large strings can consume excessive memory. Control this through input buffer size limits.

  3. Handle unknown keys: Use error_on_unknown_keys = false if you want to ignore unexpected fields, or keep it true (default) to reject messages with unknown structure.

// Reject messages with unknown keys (default behavior)
constexpr glz::opts strict_opts{.error_on_unknown_keys = true};

// Or allow unknown keys to be ignored
constexpr glz::opts lenient_opts{.error_on_unknown_keys = false};

Error Handling

Always check return values when parsing untrusted data:

auto ec = glz::read_beve(obj, buffer);
if (ec) {
    // Parsing failed - do not use obj
    std::cerr << glz::format_error(ec, buffer) << '\n';
    return;
}
// Safe to use obj

Error codes that may indicate malicious input:

Error Code Description
invalid_length Length exceeds allowed limit (buffer size or user-configured max)
unexpected_end Buffer truncated during parsing
syntax_error Invalid data structure or type mismatch
parse_error Malformed data that doesn't match expected format

Reporting Security Issues

If you discover a security vulnerability in Glaze, please report it responsibly by opening an issue at https://github.com/stephenberry/glaze/issues.