Skip to content

Commit 00a8035

Browse files
committed
fix(v3): reject non-array ore 'ob' payloads at the extractor boundary
has_ore_block_256 used "val ->> 'ob' IS NOT NULL", which stringifies a scalar/object 'ob' and reports it present. ore_block_256 then fed the malformed payload into jsonb_array_to_ore_block_256, which returns NULL instead of raising, silently degrading a structurally invalid ORE term into a NULL comparison/index term. Tighten the guard to require a JSON array (jsonb_typeof(val->'ob') = 'array'); a present-but-non-array 'ob' now RAISEs at the extractor boundary. '{}' (absent ob) and '{"ob": null}' (JSON null) remain absent (false). Adds T5 characterization cases for the scalar/object 'ob' presence checks and the extractor RAISE. Addresses CodeRabbit review thread on PR #276.
1 parent d54918d commit 00a8035

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

src/v3/sem/ore_block_256/functions.sql

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
--! This deliberately diverges from the v2 plpgsql equivalent (intentionally
2323
--! left unchanged): the `CASE WHEN jsonb_typeof(val) = 'array'` guard only
2424
--! evaluates the array path for an array, so a non-array JSON scalar returns
25-
--! NULL here instead of raising. The sole caller passes `val->'ob'`, always an
26-
--! array or JSON null, so the divergence is unreachable in practice; JSON null
27-
--! and empty array still return NULL exactly as before.
25+
--! NULL here instead of raising. The sole caller (`ore_block_256`) only reaches
26+
--! this when `has_ore_block_256(val)` is true, which now requires `val->'ob'`
27+
--! to be a JSON array, so the non-array branch is unreachable in practice;
28+
--! empty array still returns NULL exactly as before (pinned by T7).
2829
CREATE FUNCTION eql_v3.jsonb_array_to_ore_block_256(val jsonb)
2930
RETURNS eql_v3.ore_block_256
3031
IMMUTABLE
@@ -67,16 +68,24 @@ AS $$
6768
$$ LANGUAGE plpgsql;
6869

6970

70-
--! @brief Check if JSONB payload contains ORE block index term
71+
--! @brief Check if JSONB payload contains an ORE block index term
7172
--! @param val jsonb containing encrypted EQL payload
72-
--! @return boolean True if 'ob' field is present and non-null
73+
--! @return boolean True only if the 'ob' field is present and is a JSON array
74+
--! @note A well-formed ORE index term is always a JSON array of block terms, so
75+
--! this guard treats a present-but-non-array `ob` (a scalar or object) as
76+
--! absent. That makes the extractor `ore_block_256(val)` RAISE on a
77+
--! structurally invalid `ob` payload at the boundary instead of silently
78+
--! degrading it to a NULL index term in `jsonb_array_to_ore_block_256`. The
79+
--! previous `val ->> 'ob' IS NOT NULL` form stringified scalars/objects and so
80+
--! reported them as present. `{}` (absent `ob`) and `{"ob": null}` (JSON null)
81+
--! both remain `false`.
7382
CREATE FUNCTION eql_v3.has_ore_block_256(val jsonb)
7483
RETURNS boolean
7584
IMMUTABLE STRICT PARALLEL SAFE
7685
SET search_path = pg_catalog, extensions, public
7786
AS $$
7887
BEGIN
79-
RETURN val ->> 'ob' IS NOT NULL;
88+
RETURN COALESCE(jsonb_typeof(val -> 'ob') = 'array', false);
8089
END;
8190
$$ LANGUAGE plpgsql;
8291

tests/sqlx/tests/encrypted_domain/family/sem.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ async fn ore_terms_array_null_and_empty_base_cases(pool: PgPool) -> Result<()> {
178178
}
179179

180180
/// T5 — SEM presence checks (`has_ore_block_256`, `has_hmac_256`), the
181-
/// extractor's missing-`ob` RAISE, and its NULL-jsonb short-circuit.
181+
/// extractor's missing-`ob` and non-array-`ob` RAISEs, and its NULL-jsonb
182+
/// short-circuit.
182183
#[sqlx::test]
183184
async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<()> {
184185
let bool_cases = [
@@ -187,11 +188,20 @@ async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<()
187188
true,
188189
),
189190
(r#"SELECT eql_v3.has_ore_block_256('{}'::jsonb)"#, false),
190-
// json-null `ob` → `->>` yields NULL → absent.
191+
// json-null `ob` is typed `'null'`, not `'array'` → absent.
191192
(
192193
r#"SELECT eql_v3.has_ore_block_256('{"ob":null}'::jsonb)"#,
193194
false,
194195
),
196+
// Present-but-non-array `ob` is rejected as absent: a well-formed ORE
197+
// term is always a JSON array of block terms, so a scalar and an object
198+
// both → false. This is the boundary that makes `ore_block_256` RAISE on
199+
// a malformed `ob` instead of degrading it to a NULL index term.
200+
(r#"SELECT eql_v3.has_ore_block_256('{"ob":5}'::jsonb)"#, false),
201+
(
202+
r#"SELECT eql_v3.has_ore_block_256('{"ob":{}}'::jsonb)"#,
203+
false,
204+
),
195205
(r#"SELECT eql_v3.has_hmac_256('{"hm":"abc"}'::jsonb)"#, true),
196206
(r#"SELECT eql_v3.has_hmac_256('{}'::jsonb)"#, false),
197207
];
@@ -209,6 +219,16 @@ async fn sem_presence_checks_and_missing_ob_behaviour(pool: PgPool) -> Result<()
209219
)
210220
.await?;
211221

222+
// Present-but-non-array `ob` → RAISE at the extractor boundary, NOT a silent
223+
// NULL index term (`has_ore_block_256` reports it absent).
224+
assert_raises(
225+
&pool,
226+
r#"SELECT eql_v3.ore_block_256('{"ob":5}'::jsonb)"#,
227+
&[],
228+
"Expected an ore index (ob) value",
229+
)
230+
.await?;
231+
212232
// NULL jsonb → NULL composite (STRICT short-circuit), NOT a raise.
213233
let is_null: bool = sqlx::query_scalar("SELECT eql_v3.ore_block_256(NULL::jsonb) IS NULL")
214234
.fetch_one(&pool)

0 commit comments

Comments
 (0)