summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgarren smith <garren.smith@gmail.com>2017-06-22 15:06:59 +0200
committerGitHub <noreply@github.com>2017-06-22 15:06:59 +0200
commit2f539e003c88b8c76c42a39e6c53b9e61bc783f8 (patch)
treec741c57afbe310d95823d3c121cbdfc92ec5c04c
parent90db262d00ee9066f94ac842fe6a043adbb1eeaf (diff)
downloadcouchdb-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.ini9
-rw-r--r--src/chttpd/src/chttpd.erl3
-rw-r--r--src/chttpd/src/chttpd_xframe_options.erl99
-rw-r--r--src/chttpd/test/chttpd_xframe_test.erl84
-rw-r--r--src/couch/include/couch_db.hrl1
-rw-r--r--src/couch/src/couch_httpd.erl8
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