couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From rnew...@apache.org
Subject couch commit: updated refs/heads/master to d5a3fc2
Date Wed, 05 Aug 2015 13:27:08 GMT
Repository: couchdb-couch
Updated Branches:
  refs/heads/master 4708e3271 -> d5a3fc2a5


Add CSRF protection

If the request parameter `csrf` is set to `true` when successfully
acquiring a session cookie from `_session` an additional cookie
(`Csrf-token`) is returned. All requests that send this new cookie
must also send a header (`X-Csrf-Token`) with the same value. If the
cookie is sent and the header is missing or different, a 403 response
is generated.

Note that the CSRF token is signed by the server so tampering is
detected and also results in a 403 response.

closes COUCHDB-2762


Project: http://git-wip-us.apache.org/repos/asf/couchdb-couch/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-couch/commit/d5a3fc2a
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-couch/tree/d5a3fc2a
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-couch/diff/d5a3fc2a

Branch: refs/heads/master
Commit: d5a3fc2a511baff81bdbde64973fe0ca598942d5
Parents: 4708e32
Author: Robert Newson <rnewson@apache.org>
Authored: Fri Jul 31 16:25:36 2015 +0100
Committer: Robert Newson <rnewson@apache.org>
Committed: Tue Aug 4 11:52:51 2015 +0100

----------------------------------------------------------------------
 src/couch_httpd.erl      |  18 +++--
 src/couch_httpd_csrf.erl | 179 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 191 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/d5a3fc2a/src/couch_httpd.erl
----------------------------------------------------------------------
diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl
index 57ca39a..16fffcd 100644
--- a/src/couch_httpd.erl
+++ b/src/couch_httpd.erl
@@ -295,6 +295,7 @@ handle_request_int(MochiReq, DefaultFun,
     {ok, Resp} =
     try
         validate_host(HttpReq),
+        couch_httpd_csrf:validate(HttpReq),
         check_request_uri_length(RawUri),
         case couch_httpd_cors:is_preflight_request(HttpReq) of
         #httpd{} ->
@@ -488,8 +489,9 @@ serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot,
     ResponseHeaders = server_header()
         ++ couch_httpd_auth:cookie_auth_header(Req, [])
         ++ ExtraHeaders,
-    {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
-            couch_httpd_cors:cors_headers(Req, ResponseHeaders))}.
+    ResponseHeaders1 = couch_httpd_cors:cors_headers(Req, ResponseHeaders),
+    ResponseHeaders2 = couch_httpd_csrf:headers(Req, ResponseHeaders1),
+    {ok, MochiReq:serve_file(RelativePath, DocumentRoot, ResponseHeaders2)}.
 
 qs_value(Req, Key) ->
     qs_value(Req, Key, undefined).
@@ -661,7 +663,8 @@ start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length)
->
     Headers1 = Headers ++ server_header() ++
                couch_httpd_auth:cookie_auth_header(Req, Headers),
     Headers2 = couch_httpd_cors:cors_headers(Req, Headers1),
-    Resp = MochiReq:start_response_length({Code, Headers2, Length}),
+    Headers3 = couch_httpd_csrf:headers(Req, Headers2),
+    Resp = MochiReq:start_response_length({Code, Headers3, Length}),
     case MochiReq:get(method) of
     'HEAD' -> throw({http_head_abort, Resp});
     _ -> ok
@@ -674,7 +677,8 @@ start_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
     CookieHeader = couch_httpd_auth:cookie_auth_header(Req, Headers),
     Headers1 = Headers ++ server_header() ++ CookieHeader,
     Headers2 = couch_httpd_cors:cors_headers(Req, Headers1),
-    Resp = MochiReq:start_response({Code, Headers2}),
+    Headers3 = couch_httpd_csrf:headers(Req, Headers2),
+    Resp = MochiReq:start_response({Code, Headers3}),
     case MochiReq:get(method) of
         'HEAD' -> throw({http_head_abort, Resp});
         _ -> ok
@@ -709,7 +713,8 @@ start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers)
->
     Headers2 = Headers1 ++ server_header() ++
                couch_httpd_auth:cookie_auth_header(Req, Headers1),
     Headers3 = couch_httpd_cors:cors_headers(Req, Headers2),
-    Resp = MochiReq:respond({Code, Headers3, chunked}),
+    Headers4 = couch_httpd_csrf:headers(Req, Headers3),
+    Resp = MochiReq:respond({Code, Headers4, chunked}),
     case MochiReq:get(method) of
     'HEAD' -> throw({http_head_abort, Resp});
     _ -> ok
@@ -740,8 +745,9 @@ send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
     Headers2 = Headers1 ++ server_header() ++
                couch_httpd_auth:cookie_auth_header(Req, Headers1),
     Headers3 = couch_httpd_cors:cors_headers(Req, Headers2),
+    Headers4 = couch_httpd_csrf:headers(Req, Headers3),
 
-    {ok, MochiReq:respond({Code, Headers3, Body})}.
+    {ok, MochiReq:respond({Code, Headers4, Body})}.
 
 send_method_not_allowed(Req, Methods) ->
     send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, ?l2b("Only
" ++ Methods ++ " allowed")).

http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/d5a3fc2a/src/couch_httpd_csrf.erl
----------------------------------------------------------------------
diff --git a/src/couch_httpd_csrf.erl b/src/couch_httpd_csrf.erl
new file mode 100644
index 0000000..5f8c708
--- /dev/null
+++ b/src/couch_httpd_csrf.erl
@@ -0,0 +1,179 @@
+% 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.
+
+%% This module provides optional CSRF protection to any client
+%%
+%% Clients should use the following pseudo code;
+%% if (hasCookie("CouchDB-CSRF")) {
+%%   setRequestHeader("X-CouchDB-CSRF", cookieValue("CouchDB-CSRF"));
+%% } else {
+%%   setRequestHeader("X-CouchDB-CSRF", "true")
+%% }
+%%
+%% If CouchDB sees the CouchDB-CSRF cookie then it checks its validity
+%% and whether the X-CouchDB-CSRF request header exists and matches.
+%% A 403 is returned if those checks fail.
+%% If CouchDB does not see the CouchDB-CSRF cookie but does see
+%% the X-CouchDB-CSRF header with value "true", a CouchDB-CSRF cookie
+%% is generated and returned.
+
+-module(couch_httpd_csrf).
+
+-export([validate/1, headers/2]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+validate(#httpd{} = Req) ->
+    Cookie = csrf_from_req(Req),
+    Header = couch_httpd:header_value(Req, "X-CouchDB-CSRF"),
+    case {Cookie, Header} of
+        {undefined, undefined} ->
+            ok;
+        {undefined, "true"} ->
+            ok;
+        {"deleted", "true"} ->
+            ok;
+        {undefined, _} ->
+            throw({forbidden, <<"CSRF header sent without Cookie">>});
+        {Csrf, Csrf} ->
+            ok = validate(Csrf);
+        _ ->
+            throw({forbidden, <<"CSRF Cookie/Header mismatch">>})
+    end;
+%% Check that we generated this CSRF token
+validate(Csrf) when is_list(Csrf) ->
+    case decode_cookie(Csrf) of
+        malformed ->
+            throw({forbidden, <<"Malformed CSRF Cookie">>});
+        Cookie ->
+            case validate_cookie(Cookie) of
+                true ->
+                    ok;
+                false ->
+                    throw({forbidden, <<"CSRF Cookie invalid or expired">>})
+            end
+    end.
+
+
+headers(#httpd{} = Req, Headers) ->
+    Header = couch_httpd:header_value(Req, "X-CouchDB-CSRF"),
+    case {csrf_from_req(Req), csrf_in_headers(Headers), Header} of
+        {undefined, false, "true"} ->
+            [make_cookie() | Headers];
+        {"deleted", false, "true"} ->
+            [make_cookie() | Headers];
+        {Csrf, false, Csrf} when Csrf /= undefined ->
+            case decode_cookie(Csrf) of
+                malformed ->
+                    [delete_cookie() | Headers];
+                Cookie ->
+                    case validate_cookie(Cookie) of
+                        true ->
+                            case refresh_cookie(Cookie) of
+                                true ->
+                                    valid([make_cookie() | Headers]);
+                                false ->
+                                    valid(Headers)
+                            end;
+                        false ->
+                            [delete_cookie() | Headers]
+                    end
+            end;
+        _ ->
+            Headers
+    end.
+
+
+make_cookie() ->
+    Secret = ?l2b(ensure_csrf_secret()),
+    Token = crypto:rand_bytes(8),
+    Timestamp = timestamp(),
+    Data = <<Token/binary, Timestamp:32>>,
+    Hmac = crypto:sha_mac(Secret, Data),
+    mochiweb_cookies:cookie("CouchDB-CSRF",
+        couch_util:encodeBase64Url(<<Data/binary, Hmac/binary>>),
+        [{path, "/"}, {max_age, max_age()}]).
+
+
+delete_cookie() ->
+    mochiweb_cookies:cookie("CouchDB-CSRF", "deleted",
+        [{path, "/"}, {max_age, 0}]).
+
+csrf_from_req(#httpd{} = Req) ->
+    case couch_httpd:header_value(Req, "Cookie") of
+        undefined ->
+            undefined;
+        Value ->
+            Cookies = mochiweb_cookies:parse_cookie(Value),
+            couch_util:get_value("CouchDB-CSRF", Cookies)
+    end.
+
+
+valid(Headers) ->
+    case lists:keyfind("X-CouchDB-CSRF-Valid", 1, Headers) of
+        false ->
+            [{"X-CouchDB-CSRF-Valid", "true"} | Headers];
+        _ ->
+            Headers
+    end.
+
+csrf_in_headers(Headers) when is_list(Headers) ->
+    lists:any(fun is_csrf_header/1, Headers).
+
+
+is_csrf_header({"Set-Cookie", [$C, $o, $u, $c, $h, $D, $B, $-, $C, $S, $R, $F, $= | _]})
->
+    true;
+is_csrf_header(_) ->
+    false.
+
+
+ensure_csrf_secret() ->
+    case config:get("couch_httpd_csrf", "secret", undefined) of
+        undefined ->
+            NewSecret = ?b2l(couch_uuids:random()),
+            config:set("couch_httpd_csrf", "secret", NewSecret),
+            NewSecret;
+        Secret -> Secret
+    end.
+
+
+decode_cookie(Cookie) ->
+    try
+        Cookie1 = couch_util:decodeBase64Url(Cookie),
+        <<Token:8/binary, Timestamp:32, Hmac:20/binary>> = Cookie1,
+        {Token, Timestamp, Hmac}
+    catch
+        error:_ ->
+            malformed
+    end.
+
+
+validate_cookie({Token, Timestamp, ActualHmac}) ->
+    Secret = ensure_csrf_secret(),
+    ExpectedHmac = crypto:sha_mac(Secret, <<Token/binary, Timestamp:32>>),
+    MaxAge = max_age(),
+    Expired = Timestamp + MaxAge < timestamp(),
+    couch_passwords:verify(ActualHmac, ExpectedHmac) and not Expired.
+
+
+refresh_cookie({_, Timestamp, _}) ->
+    MaxAge = max_age(),
+    TimeLeft = Timestamp + MaxAge - timestamp(),
+    TimeLeft < MaxAge * 0.5.
+
+
+max_age() ->
+    config:get_integer("couch_httpd_csrf", "timeout", 3600).
+
+
+timestamp() ->
+    couch_httpd_auth:make_cookie_time().


Mime
View raw message