diff options
author | garren smith <garren.smith@gmail.com> | 2017-06-22 15:06:59 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-22 15:06:59 +0200 |
commit | 2f539e003c88b8c76c42a39e6c53b9e61bc783f8 (patch) | |
tree | c741c57afbe310d95823d3c121cbdfc92ec5c04c | |
parent | 90db262d00ee9066f94ac842fe6a043adbb1eeaf (diff) | |
download | couchdb-2f539e003c88b8c76c42a39e6c53b9e61bc783f8.tar.gz |
Add X-Frame-Options (#582)
Adds X-Frame-Options support to help protect against clickjacking.
X-Frame-Options is configurable via the config and allows for DENY,
SAMEORIGIN and ALLOW-FROM
-rw-r--r-- | rel/overlay/etc/default.ini | 9 | ||||
-rw-r--r-- | src/chttpd/src/chttpd.erl | 3 | ||||
-rw-r--r-- | src/chttpd/src/chttpd_xframe_options.erl | 99 | ||||
-rw-r--r-- | src/chttpd/test/chttpd_xframe_test.erl | 84 | ||||
-rw-r--r-- | src/couch/include/couch_db.hrl | 1 | ||||
-rw-r--r-- | src/couch/src/couch_httpd.erl | 8 |
6 files changed, 200 insertions, 4 deletions
diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index b92b1b7c1..163b90ea1 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -87,6 +87,7 @@ allow_jsonp = false ;socket_options = [{recbuf, 262144}, {sndbuf, 262144}, {nodelay, true}] socket_options = [{recbuf, 262144}, {sndbuf, 262144}] enable_cors = false +enable_xframe_options = false ; CouchDB can optionally enforce a maximum uri length; ; max_uri_length = 8000 ; changes_timeout = 60000 @@ -187,6 +188,14 @@ credentials = false ; List of accepted methods ; methods = +[x_frame_options] +; Settings same-origin will return X-Frame-Options: SAMEORIGIN. +; If same origin is set, it will ignore the hosts setting +; same_origin = true +; Settings hosts will return X-Frame-Options: ALLOW-FROM https://example.com/ +; List of hosts separated by a comma. * means accept all +; hosts = + [couch_httpd_oauth] ; If set to 'true', oauth token and consumer secrets will be looked up ; in the authentication database (_users). These secrets are stored in diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl index 86c2752c1..3fcb51faf 100644 --- a/src/chttpd/src/chttpd.erl +++ b/src/chttpd/src/chttpd.erl @@ -1111,7 +1111,8 @@ basic_headers(Req, Headers0) -> Headers = Headers0 ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers0), - chttpd_cors:headers(Req, Headers). + Headers1 = chttpd_xframe_options:header(Req, Headers), + chttpd_cors:headers(Req, Headers1). handle_response(Req0, Code0, Headers0, Args0, Type) -> {ok, {Req1, Code1, Headers1, Args1}} = diff --git a/src/chttpd/src/chttpd_xframe_options.erl b/src/chttpd/src/chttpd_xframe_options.erl new file mode 100644 index 000000000..9d3a554cc --- /dev/null +++ b/src/chttpd/src/chttpd_xframe_options.erl @@ -0,0 +1,99 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(chttpd_xframe_options). + +-export([ + header/2, + header/3 +]). + +-define(DENY, "DENY"). +-define(SAMEORIGIN, "SAMEORIGIN"). +-define(ALLOWFROM, "ALLOW-FROM "). + + +-include_lib("couch/include/couch_db.hrl"). + +% X-Frame-Options protects against clickjacking by limiting whether a response can be used in a +% <frame>, <iframe> or <object>. + +header(Req, Headers) -> + header(Req, Headers, get_xframe_config(Req)). + + + +header(Req, Headers, Config) -> + case lists:keyfind(enabled, 1, Config) of + {enabled, true} -> + generate_xframe_header(Req, Headers, Config); + _ -> + Headers + end. + + + +generate_xframe_header(Req, Headers, Config) -> + XframeOption = case lists:keyfind(same_origin, 1, Config) of + {same_origin, true} -> + ?SAMEORIGIN; + _ -> + check_host(Req, Config) + end, + [{"X-Frame-Options", XframeOption } | Headers]. + + + +check_host(#httpd{mochi_req = MochiReq} = Req, Config) -> + Host = couch_httpd_vhost:host(MochiReq), + case Host of + [] -> + ?DENY; + Host -> + FullHost = chttpd:absolute_uri(Req, ""), + AcceptedHosts = get_accepted_hosts(Config), + AcceptAll = ["*"] =:= AcceptedHosts, + case AcceptAll orelse lists:member(FullHost, AcceptedHosts) of + true -> ?ALLOWFROM ++ FullHost; + false -> ?DENY + end + end. + + + +get_xframe_config(#httpd{xframe_config = undefined}) -> + EnableXFrame = config:get("httpd", "enable_xframe_options", "false") =:= "true", + SameOrigin = config:get("x_frame_options", "same_origin", "false") =:= "true", + AcceptedHosts = case config:get("x_frame_options", "hosts") of + undefined -> []; + Hosts -> split_list(Hosts) + end, + [ + {enabled, EnableXFrame}, + {same_origin, SameOrigin}, + {hosts, AcceptedHosts} + ]; +get_xframe_config(#httpd{xframe_config = Config}) -> + Config. + + + +get_accepted_hosts(Config) -> + case lists:keyfind(hosts, 1, Config) of + false -> []; + {hosts, AcceptedHosts} -> AcceptedHosts + end. + + + +split_list(S) -> + re:split(S, "\\s*,\\s*", [trim, {return, list}]). diff --git a/src/chttpd/test/chttpd_xframe_test.erl b/src/chttpd/test/chttpd_xframe_test.erl new file mode 100644 index 000000000..1272c198c --- /dev/null +++ b/src/chttpd/test/chttpd_xframe_test.erl @@ -0,0 +1,84 @@ +-module(chttpd_xframe_test). + + +-include_lib("couch/include/couch_db.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +setup() -> + ok = meck:new(config), + ok = meck:expect(config, get, fun(_, _, _) -> "X-Forwarded-Host" end), + ok. + +teardown(_) -> + meck:unload(config). + +mock_request() -> + Headers = mochiweb_headers:make([{"Host", "examples.com"}]), + MochiReq = mochiweb_request:new(nil, 'GET', '/', {1, 1}, Headers), + #httpd{mochi_req = MochiReq}. + +config_disabled() -> + [ + {enabled, false} + ]. + +config_sameorigin() -> + [ + {enabled, true}, + {same_origin, true} + ]. + +config_wildcard() -> + [ + {enabled, true}, + {same_origin, false}, + {hosts, ["*"]} + ]. + +config_specific_hosts() -> + [ + {enabled, true}, + {same_origin, false}, + {hosts, ["http://couchdb.org", "http://examples.com"]} + ]. + +config_diffent_specific_hosts() -> + [ + {enabled, true}, + {same_origin, false}, + {hosts, ["http://couchdb.org"]} + ]. + +no_header_if_xframe_disabled_test() -> + Headers = chttpd_xframe_options:header(mock_request(), [], config_disabled()), + ?assertEqual(Headers, []). + +enabled_with_same_origin_test() -> + Headers = chttpd_xframe_options:header(mock_request(), [], config_sameorigin()), + ?assertEqual(Headers, [{"X-Frame-Options", "SAMEORIGIN"}]). + + +xframe_host_test_() -> + { + "xframe host tests", + { + foreach, fun setup/0, fun teardown/1, + [ + fun allow_with_wildcard_host/1, + fun allow_with_specific_host/1, + fun deny_with_different_host/1 + ] + } + }. + +allow_with_wildcard_host(_) -> + Headers = chttpd_xframe_options:header(mock_request(), [], config_wildcard()), + ?_assertEqual([{"X-Frame-Options", "ALLOW-FROM http://examples.com"}], Headers). + +allow_with_specific_host(_) -> + Headers = chttpd_xframe_options:header(mock_request(), [], config_specific_hosts()), + ?_assertEqual([{"X-Frame-Options", "ALLOW-FROM http://examples.com"}], Headers). + +deny_with_different_host(_) -> + Headers = chttpd_xframe_options:header(mock_request(), [], config_diffent_specific_hosts()), + ?_assertEqual([{"X-Frame-Options", "DENY"}], Headers). diff --git a/src/couch/include/couch_db.hrl b/src/couch/include/couch_db.hrl index e7cd85d09..7049c6e5f 100644 --- a/src/couch/include/couch_db.hrl +++ b/src/couch/include/couch_db.hrl @@ -101,6 +101,7 @@ original_method, nonce, cors_config, + xframe_config, qs }). diff --git a/src/couch/src/couch_httpd.erl b/src/couch/src/couch_httpd.erl index 380b73f01..456e82ef5 100644 --- a/src/couch/src/couch_httpd.erl +++ b/src/couch/src/couch_httpd.erl @@ -754,7 +754,8 @@ send_response(Req, Code, Headers0, Body) -> send_response_no_cors(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> Headers1 = http_1_0_keep_alive(MochiReq, Headers), Headers2 = basic_headers_no_cors(Req, Headers1), - Resp = handle_response(Req, Code, Headers2, Body, respond), + Headers3 = chttpd_xframe_options:header(Req, Headers2), + Resp = handle_response(Req, Code, Headers3, Body, respond), log_response(Code, Body), {ok, Resp}. @@ -1136,8 +1137,9 @@ add_headers(Req, Headers0) -> http_1_0_keep_alive(Req, Headers). basic_headers(Req, Headers0) -> - Headers = basic_headers_no_cors(Req, Headers0), - chttpd_cors:headers(Req, Headers). + Headers1 = basic_headers_no_cors(Req, Headers0), + Headers2 = chttpd_xframe_options:header(Req, Headers1), + chttpd_cors:headers(Req, Headers2). basic_headers_no_cors(Req, Headers) -> Headers |