Skip to content

Commit 0788d06

Browse files
committed
feat: Validate JS and Mango validate_doc_update on PUT /db/_design/doc
Currently, when a design doc is updated, we validate the `map` and `reduce` fields, but not `validate_doc_update`. Instead, trying to update any other doc while an invalid `validate_doc_update` exists will trigger an error. This comment makes VDU validation more 'eager' by performing it when the ddoc itself is updated. Normal doc writes will still trigger an error if an invalid `validate_doc_update` already exists, but now we try to prevent this happening by validating VDUs when they are first created.
1 parent 4f89067 commit 0788d06

11 files changed

Lines changed: 154 additions & 24 deletions

File tree

rel/overlay/etc/default.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ view_index_dir = {{view_index_dir}}
132132
; Javascript engine. The choices are: spidermonkey and quickjs
133133
;js_engine = spidermonkey
134134

135+
; When set to "true", the `validate_doc_update` field will be validated when
136+
; design documents are updated. For `javascript` design docs, the field must
137+
; contain a well-formed JavaScript function, and for `query` design docs it
138+
; must contain a Mango selector that is correctly structured to validate
139+
; document updates.
140+
validate_vdu = true
141+
135142
; Use cfile. This is a C-based file I/O module that can execute parallel file
136143
; read calls. The regular Erlang VM file module, at least as of OTP 28 forces
137144
; all file operations to go through a single controlling process which can

share/server/dispatch-quickjs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ globalThis.dispatch = function(line) {
162162
case "reset":
163163
State.reset.apply(null, cmd);
164164
break;
165+
case "validate_fun":
166+
State.validateFun.apply(null, cmd);
167+
break;
165168
case "add_fun":
166169
State.addFun.apply(null, cmd);
167170
break;

share/server/loop.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ var Loop = function() {
126126
"ddoc" : DDoc.ddoc,
127127
// "view" : Views.handler,
128128
"reset" : State.reset,
129+
"validate_fun": State.validateFun,
129130
"add_fun" : State.addFun,
130131
"add_lib" : State.addLib,
131132
"map_doc" : Views.mapDoc,

share/server/state.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@
1010
// License for the specific language governing permissions and limitations under
1111
// the License.
1212

13+
var makefun = function(newFun, option) {
14+
switch (option) {
15+
case 'nouveau':
16+
var sandbox = create_nouveau_sandbox();
17+
break;
18+
default:
19+
var sandbox = create_dreyfus_sandbox();
20+
break;
21+
}
22+
return Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox);
23+
}
24+
1325
var State = {
1426
reset : function(config) {
1527
// clear the globals and run gc
@@ -19,17 +31,15 @@ var State = {
1931
gc();
2032
print("true"); // indicates success
2133
},
34+
validateFun : function(newFun, option) {
35+
// Validate a function but do not store it
36+
makefun(newFun, option);
37+
print("true");
38+
},
2239
addFun : function(newFun, option) {
2340
// Compile to a function and add it to funs array
24-
switch (option) {
25-
case 'nouveau':
26-
var sandbox = create_nouveau_sandbox();
27-
break;
28-
default:
29-
var sandbox = create_dreyfus_sandbox();
30-
break;
31-
}
32-
State.funs.push(Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox));
41+
var fun = makefun(newFun, option);
42+
State.funs.push(fun);
3343
print("true");
3444
},
3545
addLib : function(lib) {

src/couch/src/couch_query_servers.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@
4242

4343
try_compile(Proc, FunctionType, FunctionName, FunctionSource) ->
4444
try
45-
proc_prompt(Proc, [<<"add_fun">>, FunctionSource]),
45+
case FunctionType of
46+
validate_doc_update ->
47+
proc_prompt(Proc, [<<"validate_fun">>, FunctionSource]);
48+
_ ->
49+
proc_prompt(Proc, [<<"add_fun">>, FunctionSource])
50+
end,
4651
ok
4752
catch
4853
{compilation_error, E} ->

src/couch_mrview/src/couch_mrview.erl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,11 @@ validate(Db, DDoc) ->
248248
ok
249249
end,
250250

251-
try Views =/= [] andalso couch_query_servers:get_os_process(Lang) of
252-
false ->
253-
ok;
251+
try couch_query_servers:get_os_process(Lang) of
254252
Proc ->
255253
try
256-
lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views)
254+
lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views),
255+
validate_vdu(Proc, DDoc)
257256
after
258257
couch_query_servers:ret_os_process(Proc)
259258
end
@@ -263,6 +262,21 @@ validate(Db, DDoc) ->
263262
ok
264263
end.
265264

265+
validate_vdu(Proc, #doc{body = {Props}}) ->
266+
case config:get_boolean("couchdb", "validate_vdu", false) of
267+
true ->
268+
case couch_util:get_value(<<"validate_doc_update">>, Props) of
269+
undefined ->
270+
ok;
271+
VDU ->
272+
couch_query_servers:try_compile(
273+
Proc, validate_doc_update, <<"validate_doc_update">>, VDU
274+
)
275+
end;
276+
_ ->
277+
ok
278+
end.
279+
266280
check_rank(<<N/binary>>) ->
267281
try binary_to_integer(N) of
268282
Val when Val >= 1 andalso Val =< ?MAX_RANK ->

src/couch_scanner/test/eunit/couch_scanner_test.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ setup() ->
9595
#{from => <<"x">>, to => <<"y">>}
9696
],
9797
updates => #{u1 => <<"function(d,r){return [];}">>},
98-
validate_doc_update => <<"function(n,o,u,s){return true;">>
98+
validate_doc_update => <<"function(n,o,u,s){return true;}">>
9999
}),
100100
ok = add_doc(DbName2, ?DOC3, #{foo3 => bax}),
101101
ok = add_doc(DbName2, ?DOC4, #{foo4 => baw, <<>> => this_is_ok_apparently}),

src/docs/src/config/couchdb.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ Base CouchDB Options
258258
[couchdb]
259259
js_engine = spidermonkey
260260

261+
.. config:option:: validate_vdu :: Enable checking of ``validate_doc_update``
262+
263+
.. versionadded:: TODO
264+
265+
When set to ``true``, the ``validate_doc_update`` field will be
266+
validated when design documents are updated. For ``javascript`` design
267+
docs, the field must contain a well-formed JavaScript function, and for
268+
``query`` design docs it must contain a Mango selector that is correctly
269+
structured to validate document updates. ::
270+
271+
[couchdb]
272+
validate_vdu = true
273+
261274
.. config:option:: time_seq_min_time :: Minimum time-seq threshold
262275
263276
.. versionchanged:: 3.6

src/mango/src/mango_native_proc.erl

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ set_timeout(Pid, TimeOut) when is_integer(TimeOut), TimeOut > 0 ->
4646
gen_server:call(Pid, {set_timeout, TimeOut}).
4747

4848
prompt(Pid, Data) ->
49-
gen_server:call(Pid, {prompt, Data}).
49+
case gen_server:call(Pid, {prompt, Data}) of
50+
{error, Error} ->
51+
throw(Error);
52+
Other ->
53+
Other
54+
end.
5055

5156
init(_) ->
5257
{ok, #st{}}.
@@ -95,6 +100,17 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) ->
95100
Else
96101
end,
97102
{reply, Vals, St};
103+
handle_call({prompt, [<<"validate_fun">>, Selector0 | _Rest]}, _From, St) ->
104+
try mango_selector:normalize(Selector0) of
105+
Selector ->
106+
case validate_vdu(Selector) of
107+
ok -> {reply, true, St};
108+
Error -> {reply, {error, Error}, St}
109+
end
110+
catch
111+
throw:{mango_error, mango_selector, Error} ->
112+
{reply, {error, Error}, St}
113+
end;
98114
handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) ->
99115
NewSt =
100116
case couch_util:get_value(<<"validate_doc_update">>, DDoc) of
@@ -112,12 +128,10 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
112128
Msg = [<<"validate_doc_update">>, DDocId],
113129
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St};
114130
Selector ->
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 ->
131+
case validate_vdu(Selector) of
132+
{_, Error} ->
133+
{stop, {invalid_call, Error}, {invalid_call, Error}, St};
134+
ok ->
121135
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
122136
Struct =
123137
case OldDoc of
@@ -137,6 +151,16 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
137151
handle_call(Msg, _From, St) ->
138152
{stop, {invalid_call, Msg}, {invalid_call, Msg}, St}.
139153

154+
validate_vdu(VDU) ->
155+
case mango_selector:has_allowed_fields(VDU, [<<"newDoc">>, <<"oldDoc">>]) of
156+
true ->
157+
ok;
158+
false ->
159+
Msg =
160+
<<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>,
161+
{compilation_error, Msg}
162+
end.
163+
140164
handle_cast(garbage_collect, St) ->
141165
garbage_collect(),
142166
{noreply, St};

test/elixir/test/config/suite.elixir

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,12 +526,15 @@
526526
"JavaScript VDU rejects an invalid document",
527527
"JavaScript VDU accepts a valid change",
528528
"JavaScript VDU rejects an invalid change",
529+
"invalid JavaScript VDU is detected on doc update",
530+
"invalid JavaScript VDU is rejected on design doc update",
529531
"Mango VDU accepts a valid document",
530532
"Mango VDU rejects an invalid document",
531533
"updating a Mango VDU updates its effects",
532534
"converting a Mango VDU to JavaScript updates its effects",
533535
"deleting a Mango VDU removes its effects",
534536
"Mango VDU rejects a doc if any existing ddoc fails to match",
537+
"invalid Mango VDU is detected on doc update",
535538
"Mango VDU rejects a design doc if it contains unknown fields",
536539
],
537540
"SecurityValidationTest": [

0 commit comments

Comments
 (0)