Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
ZeroDays CVE-2024-58366
High CVE-2024-58366 CVSS 8.5 SurrealDB Rust

SurrealDB Format String via JS Scripting

CVE-2024-58366 exposes a format string vulnerability in SurrealDB's rquickjs bindings, enabling memory reads or RCE for authenticated scripting users.

Offensive360 Research Team
Affects: < 1.1.1
Source Code

Overview

CVE-2024-58366 is a format string vulnerability residing in SurrealDB’s JavaScript scripting layer — specifically in the FFI boundary between Rust and the embedded rquickjs JavaScript engine. When the Exception::throw_type function constructs an error message to propagate back to the JS runtime, it passes attacker-controlled input as a format string rather than as an argument to a format call. This gives an authenticated attacker with scripting privileges a primitive to read arbitrary memory from the SurrealDB process heap and stack, and under the right conditions to redirect execution flow entirely.

The vulnerability was identified in SurrealDB versions prior to 1.1.1. SurrealDB’s scripting feature allows database users to embed JavaScript functions directly inside SurrealQL queries, making it a powerful but high-risk attack surface. Any deployment that exposes the scripting capability to untrusted or semi-trusted users — common in multi-tenant SaaS architectures built on SurrealDB — should treat this as an urgent remediation target.

The CVSS 8.5 HIGH rating reflects the combination of low attack complexity once scripting access is obtained, a high confidentiality impact through memory disclosure, and a high integrity impact through potential code execution running under the SurrealDB server process identity. The attack requires no elevated OS-level privileges beyond legitimate scripting access to the database.

Technical Analysis

SurrealDB embeds the QuickJS JavaScript engine via the rquickjs Rust crate. When JavaScript code inside a SurrealQL query throws an exception or triggers a type error, SurrealDB calls into Exception::throw_type to surface that error back to the runtime. The vulnerable pattern arises when the function forwards an attacker-controlled string directly into a C-level printf-family call — or its Rust FFI equivalent — without treating that string as a literal value.

The root cause can be reduced to this pattern in the Rust/C interop layer:

// VULNERABLE — attacker-controlled `message` is used as the format string
pub fn throw_type(ctx: &Ctx, message: &str) -> Exception {
    let c_msg = CString::new(message).unwrap();
    // Internally, rquickjs surfaces this to JS_ThrowTypeError which
    // historically accepts a printf-style format string as its first arg.
    unsafe {
        qjs::JS_ThrowTypeError(ctx.as_ptr(), c_msg.as_ptr());
    }
}

JS_ThrowTypeError in QuickJS has the signature:

JSValue JS_ThrowTypeError(JSContext *ctx, const char *fmt, ...);

Because fmt is a printf-style format string, passing a user-supplied value directly as fmt — rather than as "%s", user_input — is the classic format string bug. An attacker who controls the content of a JavaScript exception message can embed sequences like %x, %s, or %n directly:

// Malicious SurrealQL scripting payload
DEFINE FUNCTION fn::probe() {
    return function() {
        throw new TypeError("%x %x %x %x %s %s %s %s");
    };
};

When SurrealDB evaluates this function and routes the thrown TypeError through Exception::throw_type, the format string sequences are interpreted by JS_ThrowTypeError’s internal vsnprintf call against the current stack frame. %x tokens leak stack words as hexadecimal; %s tokens dereference stack-resident pointers and print the bytes found there until a null terminator; %n (where supported) writes to an attacker-influenced memory address.

The memory disclosure primitive is immediately useful for defeating ASLR: a few %x reads will leak text-segment or heap addresses, enabling an attacker to calculate absolute addresses for a subsequent exploitation stage. On platforms where %n writes are not suppressed (glibc does not suppress them by default), this escalates directly to arbitrary write and code execution within the SurrealDB process.

Impact

An attacker exploiting CVE-2024-58366 can achieve:

  • Arbitrary memory read: Stack and heap contents — including connection credentials, in-memory query results from other sessions, cryptographic key material, and internal data structures — can be leaked one format token at a time.
  • Process-level code execution: On systems where %n-style writes are permitted, the attacker can overwrite a function pointer or GOT entry to redirect execution. Code runs with the full privileges of the surreal process user, which in many deployments is a service account with broad filesystem and network access.
  • Lateral movement: A compromised SurrealDB process can be used to pivot to connected storage backends, message queues, or downstream microservices that trust the database host.

The CVSS vector AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L captures the network-accessible, low-complexity nature of the attack. The primary gate is possession of scripting privileges — a condition that is frequently met in developer-facing or analytics-facing database deployments.

How to Fix It

Upgrade immediately. SurrealDB 1.1.1 addresses this vulnerability. The fix is to pass the user-controlled message as an argument to JS_ThrowTypeError rather than as the format string itself:

// FIXED — user input is passed as a format argument, not the format string
pub fn throw_type(ctx: &Ctx, message: &str) -> Exception {
    let c_msg = CString::new(message).unwrap();
    unsafe {
        // "%s" is the literal format string; c_msg is the argument
        qjs::JS_ThrowTypeError(ctx.as_ptr(), c"%s".as_ptr(), c_msg.as_ptr());
    }
}

This one-line change eliminates the format string interpretation entirely. The user-supplied text is treated as data, not as control flow.

Upgrade commands:

# If running from Cargo
cargo update surrealdb --precise 1.1.1

# If using the official install script
curl -sSf https://install.surrealdb.com | sh -s -- --version v1.1.1

# Docker
docker pull surrealdb/surrealdb:v1.1.1

Defense in depth:

  • Disable scripting entirely if your workload does not require it. SurrealDB exposes this as a server-start flag; review your startup configuration and remove the scripting capability unless it is explicitly needed.
  • Apply the principle of least privilege to database user accounts. Scripting privileges should not be granted to roles that do not strictly require them.
  • Deploy SurrealDB under a dedicated low-privilege OS user with seccomp or AppArmor profiles restricting syscalls, limiting the blast radius of process-level compromise.

Our Take

Format string vulnerabilities have been a known exploit class since the late 1990s, yet they continue to resurface specifically at FFI boundaries — the seam between a memory-safe language and an underlying C library. This is a structurally dangerous pattern: Rust’s ownership model and type system protect Rust-managed memory admirably, but the moment you call unsafe and hand a pointer to a C function that interprets it as a format string, you have exited Rust’s safety guarantees entirely.

The lesson here is not specific to SurrealDB. Any Rust project that wraps a C library accepting printf-family format strings must audit every call site to confirm that user-controlled data is never in the format string position. This is exactly the kind of subtle, context-dependent bug that manual code review misses and that automated analysis catches consistently.

For enterprises running SurrealDB in production — particularly in multi-tenant or developer-platform contexts where scripting is a selling point — this vulnerability is a reminder that capability exposure is attack surface. Every powerful feature granted to users must be modeled as a trust boundary and threat-modeled accordingly.

Detection with SAST

This vulnerability class maps to CWE-134: Use of Externally-Controlled Format String. SAST detection targets the following patterns:

  • Taint tracking through FFI calls: Any data flow where user-supplied input reaches a printf-family function call (including sprintf, snprintf, fprintf, JS_ThrowTypeError, JS_ThrowSyntaxError, and analogues) in the format-string argument position without an intervening sanitizer or literal format wrapper.
  • unsafe block auditing: All unsafe Rust blocks that invoke external C functions should be flagged for review. Within those blocks, SAST rules inspect the argument order and type of variadic C functions to determine whether a format string position is tainted.
  • Format string literal enforcement: Rules that require the format string argument of known printf-family functions to be a string literal or a compile-time constant, rejecting any path where a runtime variable occupies that position.

Offensive360’s SAST engine applies interprocedural taint analysis across Rust/C FFI boundaries, which is precisely the capability needed to catch this class of bug. Standard intra-procedural analysis would miss it because the taint originates in the Rust caller and the dangerous consumption happens inside the C callee.

References

#format-string #rce #memory-disclosure #scripting #surrealdb

Detect this vulnerability class in your codebase

Offensive360 SAST scans your source code for CVE-2024-58366-class vulnerabilities and thousands of other patterns — across 60+ languages.