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 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
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 stringsThis 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
| 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) |
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.
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.
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
}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:
- Buffer validation: Rejects claims that exceed buffer capacity (always enabled)
- Allocation limits: Rejects allocations that exceed user-defined limits (optional)
This allows you to accept legitimately large data while protecting against excessive memory usage.
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.
- 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
}
}-
Use appropriate container types: Consider using fixed-size containers like
std::arraywhen the maximum size is known at compile time. -
Validate after parsing: Use
glz::read_constraintfor 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;
}>
);
};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;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.
- 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
When parsing JSON from untrusted sources:
-
Limit recursion depth: Deeply nested structures can cause stack overflow. Consider flattening data structures or implementing depth limits at the application level.
-
Limit string sizes: Very large strings can consume excessive memory. Control this through input buffer size limits.
-
Handle unknown keys: Use
error_on_unknown_keys = falseif you want to ignore unexpected fields, or keep ittrue(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};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 objError 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 |
If you discover a security vulnerability in Glaze, please report it responsibly by opening an issue at https://github.com/stephenberry/glaze/issues.