Skip to content

Commit 6107913

Browse files
committed
fix: Raise an error if a Mango VDU contains invalid top-level fields
1 parent 2989eaa commit 6107913

4 files changed

Lines changed: 91 additions & 14 deletions

File tree

src/mango/src/mango_native_proc.erl

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,20 +112,27 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
112112
Msg = [<<"validate_doc_update">>, DDocId],
113113
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
114114
Selector ->
115-
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
116-
Struct =
117-
case OldDoc of
118-
null -> {[{<<"newDoc">>, NewDoc}]};
119-
Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
120-
end,
121-
Reply =
122-
case mango_selector:match_failures(Selector, Struct) of
123-
[] ->
124-
true;
125-
Failures ->
126-
{[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
127-
end,
128-
{reply, Reply, St}
115+
case mango_selector:has_allowed_fields(Selector, [<<"newDoc">>, <<"oldDoc">>]) of
116+
false ->
117+
Msg =
118+
<<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>,
119+
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
120+
true ->
121+
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
122+
Struct =
123+
case OldDoc of
124+
null -> {[{<<"newDoc">>, NewDoc}]};
125+
Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]}
126+
end,
127+
Reply =
128+
case mango_selector:match_failures(Selector, Struct) of
129+
[] ->
130+
true;
131+
Failures ->
132+
{[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
133+
end,
134+
{reply, Reply, St}
135+
end
129136
end;
130137
handle_call(Msg, _From, St) ->
131138
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.

src/mango/src/mango_selector.erl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
match/2,
1818
match_failures/2,
1919
has_required_fields/2,
20+
has_allowed_fields/2,
2021
is_constant_field/2,
2122
fields/1
2223
]).
@@ -883,6 +884,39 @@ has_required_fields_int([{[{Field, Cond}]} | Rest], RequiredFields) ->
883884
has_required_fields_int(Rest, lists:delete(Field, RequiredFields))
884885
end.
885886

887+
has_allowed_fields(Selector, AllowedFields) ->
888+
Paths = lists:map(
889+
fun(Field) ->
890+
{ok, Path} = mango_util:parse_field(Field),
891+
Path
892+
end,
893+
AllowedFields
894+
),
895+
has_allowed_fields_int(Selector, Paths).
896+
897+
has_allowed_fields_int({[{Field, Cond}]}, Paths) when is_list(Field) ->
898+
Stemmed = [match_prefix(Field, Path) || Path <- Paths],
899+
Matched = [Path || Path <- Stemmed, Path /= nil],
900+
case Matched of
901+
[] -> false;
902+
M -> has_allowed_fields_int(Cond, M)
903+
end;
904+
has_allowed_fields_int({[{_Op, Conds}]}, Paths) when is_list(Conds) ->
905+
lists:all(fun(Cond) -> has_allowed_fields_int(Cond, Paths) end, Conds);
906+
has_allowed_fields_int({[{_Op, Cond}]}, Paths) ->
907+
has_allowed_fields_int(Cond, Paths);
908+
has_allowed_fields_int(_, _) ->
909+
true.
910+
911+
match_prefix([A | Rest1], [A | Rest2]) ->
912+
match_prefix(Rest1, Rest2);
913+
match_prefix([], Rest) ->
914+
Rest;
915+
match_prefix(_, []) ->
916+
[];
917+
match_prefix(_, _) ->
918+
nil.
919+
886920
% Returns true if a field in the selector is a constant value e.g. {a: {$eq: 1}}
887921
is_constant_field(Selector, Field) when not is_list(Field) ->
888922
{ok, Path} = mango_util:parse_field(Field),
@@ -1262,6 +1296,23 @@ has_required_fields_or_nested_or_false_test() ->
12621296
Normalized = normalize(Selector),
12631297
?assertEqual(false, has_required_fields(Normalized, RequiredFields)).
12641298

1299+
has_allowed_fields_test() ->
1300+
Sel1 = normalize({[{<<"a">>, 1}]}),
1301+
?assertEqual(has_allowed_fields(Sel1, [<<"a">>]), true),
1302+
?assertEqual(has_allowed_fields(Sel1, [<<"a">>, <<"b">>]), true),
1303+
?assertEqual(has_allowed_fields(Sel1, [<<"b">>]), false),
1304+
1305+
Sel2 = normalize({[{<<"$or">>, [{[{<<"a.b">>, 1}]}, {[{<<"c.d">>, 2}]}]}]}),
1306+
?assertEqual(has_allowed_fields(Sel2, [<<"a">>, <<"c">>]), true),
1307+
?assertEqual(has_allowed_fields(Sel2, [<<"a.b">>, <<"c.d">>]), true),
1308+
?assertEqual(has_allowed_fields(Sel2, [<<"a">>]), false),
1309+
1310+
Sel3 = normalize({[{<<"a">>, {[{<<"$or">>, [{[{<<"b">>, 1}]}, {[{<<"c">>, 2}]}]}]}}]}),
1311+
?assertEqual(has_allowed_fields(Sel3, [<<"a">>]), true),
1312+
?assertEqual(has_allowed_fields(Sel3, [<<"a.b">>, <<"a.c">>]), true),
1313+
?assertEqual(has_allowed_fields(Sel3, [<<"a.c">>]), false),
1314+
?assertEqual(has_allowed_fields(Sel3, [<<"b">>]), false).
1315+
12651316
check_match(Selector) ->
12661317
% Call match_int/2 to avoid ERROR for missing metric; this is confusing
12671318
% in the middle of test output.

test/elixir/test/config/suite.elixir

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@
532532
"converting a Mango VDU to JavaScript updates its effects",
533533
"deleting a Mango VDU removes its effects",
534534
"Mango VDU rejects a doc if any existing ddoc fails to match",
535+
"Mango VDU rejects a design doc if it contains unknown fields",
535536
],
536537
"SecurityValidationTest": [
537538
"Author presence and user security",

test/elixir/test/validate_doc_update_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,22 @@ defmodule ValidateDocUpdateTest do
214214
assert resp.status_code == 403
215215
assert resp.body["error"] == "forbidden"
216216
end
217+
218+
@tag :with_db
219+
test "Mango VDU rejects a design doc if it contains unknown fields", context do
220+
db = context[:db_name]
221+
222+
ddoc = %{
223+
language: "query",
224+
225+
validate_doc_update: %{
226+
"wrongField" => %{"year" => %{"$lt" => 2026}}
227+
}
228+
}
229+
resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc)
230+
assert resp.status_code == 201
231+
232+
resp = Couch.put("/#{db}/doc", body: %{"year" => 1994})
233+
assert resp.status_code == 500
234+
end
217235
end

0 commit comments

Comments
 (0)