config: tighten config validation
diff --git a/src/config.erl b/src/config.erl
index d1b67d7..640d7c2 100644
--- a/src/config.erl
+++ b/src/config.erl
@@ -43,6 +43,9 @@
-define(FEATURES, "features").
-define(TIMEOUT, 30000).
+-define(INVALID_SECTION, <<"Invalid configuration section">>).
+-define(INVALID_KEY, <<"Invalid configuration key">>).
+-define(INVALID_VALUE, <<"Invalid configuration value">>).
-record(config, {
notify_funs=[],
@@ -241,20 +244,28 @@
Resp = lists:sort((ets:tab2list(?MODULE))),
{reply, Resp, Config};
handle_call({set, Sec, Key, Val, Persist, Reason}, _From, Config) ->
- true = ets:insert(?MODULE, {{Sec, Key}, Val}),
- couch_log:notice("~p: [~s] ~s set to ~s for reason ~p",
- [?MODULE, Sec, Key, Val, Reason]),
- case {Persist, Config#config.write_filename} of
- {true, undefined} ->
- ok;
- {true, FileName} ->
- config_writer:save_to_file({{Sec, Key}, Val}, FileName);
- _ ->
- ok
- end,
- Event = {config_change, Sec, Key, Val, Persist},
- gen_event:sync_notify(config_event, Event),
- {reply, ok, Config};
+ case validate_config_update(Sec, Key, Val) of
+ {error, ValidationError} ->
+ couch_log:error("~p: [~s] ~s = '~s' rejected for reason ~p",
+ [?MODULE, Sec, Key, Val, Reason]),
+ {reply, {error, ValidationError}, Config};
+ ok ->
+ true = ets:insert(?MODULE, {{Sec, Key}, Val}),
+ couch_log:notice("~p: [~s] ~s set to ~s for reason ~p",
+ [?MODULE, Sec, Key, Val, Reason]),
+ case {Persist, Config#config.write_filename} of
+ {true, undefined} ->
+ ok;
+ {true, FileName} ->
+ config_writer:save_to_file({{Sec, Key}, Val}, FileName);
+ _ ->
+ ok
+ end,
+ Event = {config_change, Sec, Key, Val, Persist},
+ gen_event:sync_notify(config_event, Event),
+ {reply, ok, Config}
+ end;
+
handle_call({delete, Sec, Key, Persist, Reason}, _From, Config) ->
true = ets:delete(?MODULE, {Sec,Key}),
couch_log:notice("~p: [~s] ~s deleted for reason ~p",
@@ -389,6 +400,33 @@
ok
end.
+
+validate_config_update(Sec, Key, Val) ->
+ %% See https://erlang.org/doc/man/re.html &
+ %% https://pcre.org/original/doc/html/pcrepattern.html
+ %%
+ %% only characters that are actually screen-visible are allowed
+ %% tabs and spaces are allowed
+ %% no [ ] explicitly to avoid section header bypass
+ {ok, Forbidden} = re:compile("[\]\[]+", [dollar_endonly, unicode]),
+ %% Values are permitted [ ] characters as we use these in
+ %% places like mochiweb socket option lists
+ %% Values may also be empty to delete manual configuration
+ {ok, Printable} = re:compile("^[[:graph:]\t\s]*$",
+ [dollar_endonly, unicode]),
+ case {re:run(Sec, Printable),
+ re:run(Sec, Forbidden),
+ re:run(Key, Printable),
+ re:run(Key, Forbidden),
+ re:run(Val, Printable)} of
+ {{match, _}, nomatch, {match, _}, nomatch, {match, _}} -> ok;
+ {nomatch, _, _, _, _} -> {error, ?INVALID_SECTION};
+ {_, {match, _}, _, _, _} -> {error, ?INVALID_SECTION};
+ {_, _, nomatch, _, _} -> {error, ?INVALID_KEY};
+ {_, _, _, {match, _}, _} -> {error, ?INVALID_KEY};
+ {_, _, _, _, nomatch} -> {error, ?INVALID_VALUE}
+ end.
+
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -410,4 +448,37 @@
?assertEqual(0.0, to_float("+0.0")),
ok.
+validation_test() ->
+ ?assertEqual(ok, validate_config_update("section", "key", "value")),
+ ?assertEqual(ok, validate_config_update("delete", "empty_value", "")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("sect[ion", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("sect]ion", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("section\n", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("section\r", "key", "value")),
+ ?assertEqual({error, ?INVALID_SECTION},
+ validate_config_update("section\r\n", "key", "value")),
+ ?assertEqual({error, ?INVALID_KEY},
+ validate_config_update("section", "key\n", "value")),
+ ?assertEqual({error, ?INVALID_KEY},
+ validate_config_update("section", "key\r", "value")),
+ ?assertEqual({error, ?INVALID_KEY},
+ validate_config_update("section", "key\r\n", "value")),
+ ?assertEqual({error,?INVALID_VALUE},
+ validate_config_update("section", "key", "value\n")),
+ ?assertEqual({error,?INVALID_VALUE},
+ validate_config_update("section", "key", "value\r")),
+ ?assertEqual({error,?INVALID_VALUE},
+ validate_config_update("section", "key", "value\r\n")),
+ ?assertEqual({error,?INVALID_KEY},
+ validate_config_update("section", "k[ey", "value")),
+ ?assertEqual({error,?INVALID_KEY},
+ validate_config_update("section", "[key", "value")),
+ ?assertEqual({error,?INVALID_KEY},
+ validate_config_update("section", "key]", "value")),
+ ok.
+
-endif.