What went wrong

SurrealDB exposes a Headers class to embedded JavaScript. Scripts can build headers themselves or receive them from fetch(). Internally, those values are HTTP header bytes, but the JavaScript accessors have to present them as strings.

The conversion assumed that every value was visible ASCII. Methods including get(), entries(), values() and getSetCookie() called Rust's fallible to_str() conversion and then used expect(). A non-ASCII value therefore turned an ordinary representation mismatch into a panic.

The assumption was plausible for values created from simple script strings, but not for the whole class. HTTP permits header values containing other bytes, and response headers arrive from outside the JavaScript runtime. One storage type was serving two origins with different guarantees, while the accessors silently relied on the stronger one.

Why it mattered

The public advisory rates the issue Medium, with a CVSS 3.1 score of 6.5 and high availability impact. It affected SurrealDB versions before 3.3.0, but only when embedded scripting was enabled. Scripting is disabled by default.

On an affected server, any account allowed to run arbitrary queries could construct a header containing a non-ASCII value and read it back. A record user was sufficient, as was a guest where guest access was enabled. A script could also encounter the same failure while reading headers returned by an allowed network destination; restricting outbound access did not prevent the direct construction path.

SurrealDB's official release builds use an abort-on-panic profile. The failed conversion therefore did not become a JavaScript exception or terminate only one query: it ended the server process and denied service to every namespace and database until restart. The issue did not disclose or modify data.

How it was fixed

SurrealDB 3.3.0 replaces the assertion with a shared conversion function. Visible ASCII is borrowed and returned exactly as before. Other values are decoded as UTF-8, with invalid byte sequences replaced by the Unicode replacement character. Every accepted header value now has a string representation, so the JavaScript API remains unchanged without a panic path.

The public patch routes all affected accessors through that function and adds regression tests using response-style HeaderMap values. The tests cover bytes that the HTTP library accepts but to_str() rejects, preserve ordinary ASCII exactly, and verify that valid multi-byte UTF-8 remains intact.

Users should upgrade to 3.3.0 or later. Published mitigations include leaving scripting disabled, restricting who may run arbitrary queries, limiting network destinations used by stored scripts, and running the server under a restart policy. Automatic restart limits downtime but does not remove the crash.

After the patch

Crossing a language boundary changes what a value must guarantee. Rust may safely hold opaque header bytes while JavaScript expects a string; neither representation is wrong, but their intersection is smaller than either side. A fallible conversion is part of that boundary, not evidence that the input is impossible.

This suggests a useful review rule: assertions belong to invariants established locally, not to assumptions about data that arrived through a protocol, parser or foreign runtime. When a host application embeds another language, conversion failure should become a value or a local exception. It should not inherit the power to terminate the host process.

Public record