Eden
All case studies

WordPress REST batch route confusion chained to SQL injection

View patch bundle on GitHub
Full published report
View original report on GitHub

The reviewed summary below describes the scope of the fixture testing.

WordPress REST batch route confusion chained to SQL injection (CVE-2026-63030)

Source and API bundle

WordPress 6.9.0-6.9.4, 7.0.0-7.0.1 (fixed 6.9.5, 7.0.2) — cve.now month ranking entry.

Eden terminates the HTTP connection to the affected product, evaluates a declarative policy and — in tier 2 — a separately compiled native Rust module under signed module authority, before any byte reaches the application. This removes the delivery primitive from the network path. It does not patch the affected WordPress instance; the vendor's fixed release remains the durable repair.

Status: live two-tier remediation recorded 2026-11-11 on the installed eden-product-release:1.103.0-single lab container (image sha256:809293e1f0b525ac04698d1fa8d7ffdc66cbee1ff690b082ba26b4d050f08416).

The vulnerability

The REST API batch endpoint (POST /wp-json/batch/v1) suffers a route confusion issue which, chained with the author__not_in WP_Query SQL injection (CVE-2026-60137), lets an unauthenticated attacker run SQL queries and reach remote code execution. KEV-listed, exploit public.

The fleet challenge for this class is unchanged from the portfolio: the durable fix is the vendor's patched release, but fleets contain appliances and long-lived deployments that cannot upgrade inside an exploitation window (KEV-listed, public tooling). Eden's boundary removes the delivery primitive from the network path immediately, at a separately compiled enforcement point, without touching the application host.

The Eden patch

Tier 1 — declarative policy (blunt, fail-closed): deny on /wp-json/batch descendants — takes the whole batch API offline (every legitimate batch call refused).

Tier 2 — native module (surgical): gabriel_wpbatch_v1 inspects batch envelope bodies and refuses only envelopes whose subrequests carry the author__not_in query primitive — the SQLi chain element. Ordinary batch requests pass unchanged.

Module gabriel_wpbatch_v1, binary SHA-256 09d2a2a64ae0a8580e512564ab720e7d5b6eec46dbbc5ed6f40eed4c3a348d67 (source 885f9f0dd41faeaabb7f3365f8d3bd129fe9c59d029cda732faa7114c1250243), keys the author__not_in byte sequence inside batch envelope bodies.

Measured comparison (live, 2026-11-11)

ProbeBaseline (unprotected)Tier 1Tier 2 (module)
benign_batch200403200 (rce_marker ×0, sql_executed ×0)
benign_batch_two200403200 (rce_marker ×0, sql_executed ×0)
attack_author_not_in200403403 (rce_marker ×0, sql_executed ×0)

Every tier-2 refusal was measured against the same probe's live unprotected baseline; benign rows document tier-1's false positives and tier-2's zero.

Performance trace (Eden telemetry, live 2026-11-11)

Measured with Eden's built-in request telemetry (gateway.request.baseline spans, read back through /api/v1/analytics/telemetry?signal=traces), not client wall-clock. Latency Eden introduces per remediation arm for this case:

Armnmeanp50p95min–max
Tier 1 — declarative policy, blocked request (403)1530 µs23 µs56 µs19 µs–56 µs
Tier 2 — native module, benign passes through (200)21374 µs370 µs514 µs312 µs–622 µs
Tier 2 — native module, attack refused (403)14130 µs120 µs177 µs111 µs–177 µs

Eden-added latency for the tier-2 remediation: benign requests complete in 374 µs mean (p50 370 µs, p95 514 µs) in-product, module evaluation included. Honest scope: lab fixtures on one host, fresh connection per measured request, warm requests only; module reactivated shards 2/2 with a fresh signed statement per measurement; spans carry the full authority lineage (module SHA-256, operation, publication). Not the EDEN-3640 performance qualification contract.

Tested against: eden-product-release:1.103.0-single (image sha256:809293e1f0b525ac…, build target eden_service_single, tooling revision 81a0724a…). Raw data: module2/telemetry-ab/wpbatch-telemetry.json and module2/tier1-ab/wpbatch-tier1.json.

Authority lifecycle and negatives

Reached the serving process only through the full signed lifecycle (module2/evidence/wpbatch-module.json): enroll_key (Ed25519 public key), independent-host inventory, inspect (bindings derived without mapping code), host approve, signed admit (prepared, serving_now: false), activate — receipt serving_now: true, applied_shards == expected_shards (2/2). Negatives held: read-only inspect -> 403, tampered statement binding -> 400, flipped signature bit -> 403. revoke made the interlay fail closed (503, nothing forwarded, zero backend effects) and a fresh host approval plus a newly signed activation restored surgical protection.

Honest scope

The fixture replicates the delivery shape and exploit prerequisite with harmless marker effects; no real WordPress runs here. Other SQLi vectors into WP_Query, non-batch routes, and traffic that never traverses this boundary are out of scope; the module keys only the chained primitive observed in the advisory. Modules are lab-grade rules for the fixture delivery shapes, not production WordPress patches. Perf: the request-path A/B was not re-run for this batch (see README); activation telemetry measured ~0.14 s one-time load.

Cost record (per-CVE ledger)

0.6898 USD (marginal session growth over this case's window: 9,854,688 input + 718 output tokens across 27 assistant turns; windows can overlap where debugging was shared).

Evidence appendix

  • cve.now entry
  • Raw evidence: gabriel-2026-11-11/module2/evidence/wpbatch-module.json
  • Deployment: installed eden-product-release:1.103.0-single lab container (image sha256:809293e1f0b525ac04698d1fa8d7ffdc66cbee1ff690b082ba26b4d050f08416)
  • Skill: eden-vulnerability-remediation

Decision flow

Decision diagram
Rendering diagram...

The diagram shows the native rule’s byte-level branch, not a full WordPress batch router or SQL parser.

Complete native module source

The complete available source for gabriel_wpbatch_v1 is reproduced below from the reviewed repository snapshot. The source SHA-256 is 885f9f0dd41faeaabb7f3365f8d3bd129fe9c59d029cda732faa7114c1250243. This identifies the source bytes, rather than independently proving the binary build or the recorded test results. The case’s experiment status and limitations still apply, including any failed import, partial coverage, or withdrawn claim. Original source comments are preserved verbatim.

Rust
//! Gabriel module: refuse the author__not_in SQLi chain primitive in
//! WordPress REST batch envelopes (CVE-2026-63030 shape).
//!
//! CVE-2026-63030: the REST API batch endpoint's route confusion, chained
//! with the author__not_in WP_Query SQL injection (CVE-2026-60137), lets an
//! unauthenticated attacker run SQL and reach RCE. This module refuses only
//! batch envelopes whose subrequests carry the author__not_in query primitive;
//! every other batch request (normal subrequests, path/body variations)
//! passes unchanged. no_std; no allocator, storage, background work or state.

#![no_std]

use core::mem::size_of;
use core::panic::PanicInfo;
use eden_gateway_remediation::message::{Availability, MessageInput};
use eden_gateway_remediation::module::{
    ApiHeader, CONTINUE, Decision, EDIT, EditSink, Edits, Failure, ModuleOutput, NEED_MORE, REFUSE, RemediationModule,
};
use eden_gateway_remediation::native_http_v3::{self as http, Api, Input, Request};

/// The batch route in scope.
const ROUTE: &[u8] = b"POST /wp-json/batch/v1";

/// The SQLi chain primitive refused inside batch envelopes.
const CHAIN: &[u8] = b"author__not_in";

fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || haystack.len() < needle.len() {
        return None;
    }
    let mut i = 0usize;
    while i + needle.len() <= haystack.len() {
        let mut j = 0usize;
        let mut same = true;
        while j < needle.len() {
            if haystack[i + j] != needle[j] {
                same = false;
                break;
            }
            j += 1;
        }
        if same {
            return Some(i);
        }
        i += 1;
    }
    None
}

struct RefuseSqliChain;

impl RemediationModule<Request<'_>> for RefuseSqliChain {
    fn evaluate(&self, request: &Request<'_>, edits: &mut Edits<'_>) -> Result<Decision, Failure> {
        let _ = edits;
        let _ = match request.header_count() {
            Availability::Ready(count) => count,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        let message = request.message();
        if !message.prefix().starts_with(ROUTE) {
            return Ok(Decision::Continue);
        }
        if !message.is_complete() {
            return Ok(Decision::NeedMore);
        }
        let body_range = match request.body_range() {
            Availability::Ready(range) => range,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        let body = match message.bytes(body_range)? {
            Availability::Ready(bytes) => bytes,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        if find(body, CHAIN).is_some() {
            return Ok(Decision::Refuse);
        }
        Ok(Decision::Continue)
    }
}

static API: Api = Api {
    header: ApiHeader { version: http::VERSION, table_size: size_of::<Api>() as u32 },
    input_size: size_of::<Input>() as u32,
    output_size: size_of::<ModuleOutput>() as u32,
    sink_size: size_of::<EditSink>() as u32,
    message_size: size_of::<MessageInput>() as u32,
    state_bytes: 0,
    capabilities: CONTINUE | REFUSE | NEED_MORE | EDIT,
    required_inputs: 0,
    evaluate: Some(evaluate),
};

unsafe extern "C" fn evaluate(input: *const Input, sink: *const EditSink, output: *mut ModuleOutput) -> i32 {
    unsafe { http::invoke(&RefuseSqliChain, input, sink, output) }
}

#[unsafe(no_mangle)]
pub extern "C" fn eden_http_remediation_v3() -> *const Api {
    &API
}

#[panic_handler]
fn panic(_information: &PanicInfo<'_>) -> ! {
    loop {
        core::hint::spin_loop();
    }
}

Two benign batch requests returned 200 with the module active. The tested batch containing author__not_in returned 403, and the fixture recorded no new SQL-execution or execution markers.

This is an Eden gateway proof on a purpose-built fixture. The result demonstrates the exact tested requests and backend markers; it is not an independent production deployment or an end-to-end reproduction of the vendor vulnerability.

Decision flow

Decision diagram
Rendering diagram...

The diagram shows the native rule’s byte-level branch, not a full WordPress batch router or SQL parser.

The vulnerability and the test boundary

The source case links a WordPress REST batch route-confusion flaw to a separate query-injection issue. WordPress’s release identifies both vulnerabilities and fixed versions. The Eden experiment uses a synthetic batch fixture rather than a real WordPress runtime.

The fixture makes the effect measurable on both sides of the change. In the unprotected arm, the modeled attack reaches the backend and the case-specific effect is observed. In the policy and module arms, the same recorded delivery is refused and the marker does not advance. A normal request provides an availability control. These historical results are reported in the pinned case record and were not rerun for this rewrite.

The broad policy and its tradeoff

The first tier used a broad route or header refusal. It blocked the tested attack, but also refused the benign request described in the comparison. The following is a complete normalized example of the recorded rule, using the repository’s implemented schema. The exact captured API request and complete original settings were not available for comparison, so this is not represented as a byte-for-byte transcript.

JSON
{
  "version": 1,
  "profile": "http-structure-v1",
  "policy": {
    "version": 1,
    "rules": [
      {
        "scope": {
          "methods": [],
          "path": "/wp-json/batch",
          "descendants": true
        },
        "require": {
          "kind": "deny"
        }
      }
    ]
  }
}

An authorized operator publishes the declaration through the interlay settings API; the complete settings object must be preserved. Effective status and backend effects, rather than submission alone, determine whether the selected runtime is enforcing it.

The native module

For request bytes beginning POST /wp-json/batch/v1, the module waits for the complete body and refuses any occurrence of the literal byte sequence author__not_in. It does not parse a batch envelope, individual subrequests or SQL semantics.

This implementation uses the ABI v3 RemediationModule<Request> contract and can return NeedMore while request information is incomplete. It returns Refuse for the matching condition and Continue otherwise. The exact route checks, byte comparisons and early returns below define its behavior more precisely than a label such as “surgical.”

The before-and-after experiment

ProbeBaseline (unprotected)Tier 1Tier 2 (module)
benign_batch200403200 (rce_marker ×0, sql_executed ×0)
benign_batch_two200403200 (rce_marker ×0, sql_executed ×0)
attack_author_not_in200403403 (rce_marker ×0, sql_executed ×0)

The source record says the module was enrolled, inspected, independently approved, signed, admitted and activated on the installed 1.103.0 lab runtime. The activation receipt reported serving_now: true with 2/2 local shard acknowledgments. Read-only inspection, a changed binding and an invalid signature were refused. Revoking the mandatory module produced a 503 fail-closed response; a fresh approval and activation restored the module. These are recorded lifecycle results, not fleet-wide readiness or a request-latency benchmark.

What the evidence does and does not establish

The rule may also refuse legitimate uses of the same parameter, and encoded forms or other vulnerable routes may not match. The result demonstrates the recorded fixture probes, not comprehensive prevention of route confusion or SQL injection. Upgrade to the vendor-fixed releases.

No vendor application was executed in this case’s fixture test. Direct paths that do not traverse the selected Eden boundary are outside the result. Full protection claims require the real component, its actual parser and configuration, the original advisory trigger, independent effects and representative business traffic.

Complete native module source

Rust
//! Gabriel module: refuse the author__not_in SQLi chain primitive in
//! WordPress REST batch envelopes (CVE-2026-63030 shape).
//!
//! CVE-2026-63030: the REST API batch endpoint's route confusion, chained
//! with the author__not_in WP_Query SQL injection (CVE-2026-60137), lets an
//! unauthenticated attacker run SQL and reach RCE. This module refuses only
//! batch envelopes whose subrequests carry the author__not_in query primitive;
//! every other batch request (normal subrequests, path/body variations)
//! passes unchanged. no_std; no allocator, storage, background work or state.

#![no_std]

use core::mem::size_of;
use core::panic::PanicInfo;
use eden_gateway_remediation::message::{Availability, MessageInput};
use eden_gateway_remediation::module::{
    ApiHeader, CONTINUE, Decision, EDIT, EditSink, Edits, Failure, ModuleOutput, NEED_MORE, REFUSE, RemediationModule,
};
use eden_gateway_remediation::native_http_v3::{self as http, Api, Input, Request};

/// The batch route in scope.
const ROUTE: &[u8] = b"POST /wp-json/batch/v1";

/// The SQLi chain primitive refused inside batch envelopes.
const CHAIN: &[u8] = b"author__not_in";

fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || haystack.len() < needle.len() {
        return None;
    }
    let mut i = 0usize;
    while i + needle.len() <= haystack.len() {
        let mut j = 0usize;
        let mut same = true;
        while j < needle.len() {
            if haystack[i + j] != needle[j] {
                same = false;
                break;
            }
            j += 1;
        }
        if same {
            return Some(i);
        }
        i += 1;
    }
    None
}

struct RefuseSqliChain;

impl RemediationModule<Request<'_>> for RefuseSqliChain {
    fn evaluate(&self, request: &Request<'_>, edits: &mut Edits<'_>) -> Result<Decision, Failure> {
        let _ = edits;
        let _ = match request.header_count() {
            Availability::Ready(count) => count,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        let message = request.message();
        if !message.prefix().starts_with(ROUTE) {
            return Ok(Decision::Continue);
        }
        if !message.is_complete() {
            return Ok(Decision::NeedMore);
        }
        let body_range = match request.body_range() {
            Availability::Ready(range) => range,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        let body = match message.bytes(body_range)? {
            Availability::Ready(bytes) => bytes,
            Availability::NeedMore => return Ok(Decision::NeedMore),
            Availability::Unavailable => return Err(Failure),
        };
        if find(body, CHAIN).is_some() {
            return Ok(Decision::Refuse);
        }
        Ok(Decision::Continue)
    }
}

static API: Api = Api {
    header: ApiHeader { version: http::VERSION, table_size: size_of::<Api>() as u32 },
    input_size: size_of::<Input>() as u32,
    output_size: size_of::<ModuleOutput>() as u32,
    sink_size: size_of::<EditSink>() as u32,
    message_size: size_of::<MessageInput>() as u32,
    state_bytes: 0,
    capabilities: CONTINUE | REFUSE | NEED_MORE | EDIT,
    required_inputs: 0,
    evaluate: Some(evaluate),
};

unsafe extern "C" fn evaluate(input: *const Input, sink: *const EditSink, output: *mut ModuleOutput) -> i32 {
    unsafe { http::invoke(&RefuseSqliChain, input, sink, output) }
}

#[unsafe(no_mangle)]
pub extern "C" fn eden_http_remediation_v3() -> *const Api {
    &API
}

#[panic_handler]
fn panic(_information: &PanicInfo<'_>) -> ! {
    loop {
        core::hint::spin_loop();
    }
}

The separate compilation recipe and signed authority contract are in Deployment and code provenance.

Official CVE record