You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by jc...@apache.org on 2026/07/28 13:17:13 UTC
(couchdb) 11/16: fix: Raise an error if a Mango VDU contains invalid top-level fields
This is an automated email from the ASF dual-hosted git repository.
jcoglan pushed a commit to branch mango-match-failures
in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit ac963900d7150a5b2213de4463e29f6c9d57d49c
Author: James Coglan <ja...@neighbourhood.ie>
AuthorDate: Thu Apr 9 12:02:24 2026 +0100
fix: Raise an error if a Mango VDU contains invalid top-level fields
---
src/mango/src/mango_native_proc.erl | 35 ++++++++++--------
src/mango/src/mango_selector.erl | 51 +++++++++++++++++++++++++++
test/elixir/test/config/suite.elixir | 1 +
test/elixir/test/validate_doc_update_test.exs | 18 ++++++++++
4 files changed, 91 insertions(+), 14 deletions(-)
diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl
index 212c64682..92a42e21c 100644
--- a/src/mango/src/mango_native_proc.erl
+++ b/src/mango/src/mango_native_proc.erl
@@ -112,20 +112,27 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
Msg = [<<"validate_doc_update">>, DDocId],
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
Selector ->
- [NewDoc, OldDoc, _Ctx, _SecObj] = Args,
- Struct =
- case OldDoc of
- null -> {[{<<"newDoc">>, NewDoc}]};
- Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
- end,
- Reply =
- case mango_selector:match_failures(Selector, Struct) of
- [] ->
- true;
- Failures ->
- {[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
- end,
- {reply, Reply, St}
+ case mango_selector:has_allowed_fields(Selector, [<<"newDoc">>, <<"oldDoc">>]) of
+ false ->
+ Msg =
+ <<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>,
+ {stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
+ true ->
+ [NewDoc, OldDoc, _Ctx, _SecObj] = Args,
+ Struct =
+ case OldDoc of
+ null -> {[{<<"newDoc">>, NewDoc}]};
+ Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
+ end,
+ Reply =
+ case mango_selector:match_failures(Selector, Struct) of
+ [] ->
+ true;
+ Failures ->
+ {[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
+ end,
+ {reply, Reply, St}
+ end
end;
handle_call(Msg, _From, St) ->
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl
index f47ee4acf..a5796ed9e 100644
--- a/src/mango/src/mango_selector.erl
+++ b/src/mango/src/mango_selector.erl
@@ -17,6 +17,7 @@
match/2,
match_failures/2,
has_required_fields/2,
+ has_allowed_fields/2,
is_constant_field/2,
fields/1
]).
@@ -889,6 +890,39 @@ has_required_fields_int([{[{Field, Cond}]} | Rest], RequiredFields) ->
has_required_fields_int(Rest, lists:delete(Field, RequiredFields))
end.
+has_allowed_fields(Selector, AllowedFields) ->
+ Paths = lists:map(
+ fun(Field) ->
+ {ok, Path} = mango_util:parse_field(Field),
+ Path
+ end,
+ AllowedFields
+ ),
+ has_allowed_fields_int(Selector, Paths).
+
+has_allowed_fields_int({[{Field, Cond}]}, Paths) when is_list(Field) ->
+ Stemmed = [match_prefix(Field, Path) || Path <- Paths],
+ Matched = [Path || Path <- Stemmed, Path /= nil],
+ case Matched of
+ [] -> false;
+ M -> has_allowed_fields_int(Cond, M)
+ end;
+has_allowed_fields_int({[{_Op, Conds}]}, Paths) when is_list(Conds) ->
+ lists:all(fun(Cond) -> has_allowed_fields_int(Cond, Paths) end, Conds);
+has_allowed_fields_int({[{_Op, Cond}]}, Paths) ->
+ has_allowed_fields_int(Cond, Paths);
+has_allowed_fields_int(_, _) ->
+ true.
+
+match_prefix([A | Rest1], [A | Rest2]) ->
+ match_prefix(Rest1, Rest2);
+match_prefix([], Rest) ->
+ Rest;
+match_prefix(_, []) ->
+ [];
+match_prefix(_, _) ->
+ nil.
+
% Returns true if a field in the selector is a constant value e.g. {a: {$eq: 1}}
is_constant_field(Selector, Field) when not is_list(Field) ->
{ok, Path} = mango_util:parse_field(Field),
@@ -1268,6 +1302,23 @@ has_required_fields_or_nested_or_false_test() ->
Normalized = normalize(Selector),
?assertEqual(false, has_required_fields(Normalized, RequiredFields)).
+has_allowed_fields_test() ->
+ Sel1 = normalize({[{<<"a">>, 1}]}),
+ ?assertEqual(has_allowed_fields(Sel1, [<<"a">>]), true),
+ ?assertEqual(has_allowed_fields(Sel1, [<<"a">>, <<"b">>]), true),
+ ?assertEqual(has_allowed_fields(Sel1, [<<"b">>]), false),
+
+ Sel2 = normalize({[{<<"$or">>, [{[{<<"a.b">>, 1}]}, {[{<<"c.d">>, 2}]}]}]}),
+ ?assertEqual(has_allowed_fields(Sel2, [<<"a">>, <<"c">>]), true),
+ ?assertEqual(has_allowed_fields(Sel2, [<<"a.b">>, <<"c.d">>]), true),
+ ?assertEqual(has_allowed_fields(Sel2, [<<"a">>]), false),
+
+ Sel3 = normalize({[{<<"a">>, {[{<<"$or">>, [{[{<<"b">>, 1}]}, {[{<<"c">>, 2}]}]}]}}]}),
+ ?assertEqual(has_allowed_fields(Sel3, [<<"a">>]), true),
+ ?assertEqual(has_allowed_fields(Sel3, [<<"a.b">>, <<"a.c">>]), true),
+ ?assertEqual(has_allowed_fields(Sel3, [<<"a.c">>]), false),
+ ?assertEqual(has_allowed_fields(Sel3, [<<"b">>]), false).
+
check_match(Selector) ->
% Call match_int/2 to avoid ERROR for missing metric; this is confusing
% in the middle of test output.
diff --git a/test/elixir/test/config/suite.elixir b/test/elixir/test/config/suite.elixir
index 21bfe3dc0..57cf3334f 100644
--- a/test/elixir/test/config/suite.elixir
+++ b/test/elixir/test/config/suite.elixir
@@ -532,6 +532,7 @@
"converting a Mango VDU to JavaScript updates its effects",
"deleting a Mango VDU removes its effects",
"Mango VDU rejects a doc if any existing ddoc fails to match",
+ "Mango VDU rejects a design doc if it contains unknown fields",
],
"SecurityValidationTest": [
"Author presence and user security",
diff --git a/test/elixir/test/validate_doc_update_test.exs b/test/elixir/test/validate_doc_update_test.exs
index 216f0b6cf..0ebb91342 100644
--- a/test/elixir/test/validate_doc_update_test.exs
+++ b/test/elixir/test/validate_doc_update_test.exs
@@ -214,4 +214,22 @@ defmodule ValidateDocUpdateTest do
assert resp.status_code == 403
assert resp.body["error"] == "forbidden"
end
+
+ @tag :with_db
+ test "Mango VDU rejects a design doc if it contains unknown fields", context do
+ db = context[:db_name]
+
+ ddoc = %{
+ language: "query",
+
+ validate_doc_update: %{
+ "wrongField" => %{"year" => %{"$lt" => 2026}}
+ }
+ }
+ resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc)
+ assert resp.status_code == 201
+
+ resp = Couch.put("/#{db}/doc", body: %{"year" => 1994})
+ assert resp.status_code == 500
+ end
end