Remove new CSRF mechanism
Project: http://git-wip-us.apache.org/repos/asf/couchdb-couch/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-couch/commit/8c4e947e Tree: http://git-wip-us.apache.org/repos/asf/couchdb-couch/tree/8c4e947e Diff: http://git-wip-us.apache.org/repos/asf/couchdb-couch/diff/8c4e947e Branch: refs/heads/master Commit: 8c4e947ea9445545e7dc5a9d871f03fa0b32ed8c Parents: 9aff2f6 Author: Robert Newson <rnew...@apache.org> Authored: Thu Sep 10 12:27:17 2015 +0100 Committer: Robert Newson <rnew...@apache.org> Committed: Thu Sep 10 13:59:06 2015 +0100 ---------------------------------------------------------------------- src/couch_httpd.erl | 16 +-- src/couch_httpd_csrf.erl | 223 ------------------------------------------ 2 files changed, 5 insertions(+), 234 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-couch/blob/8c4e947e/src/couch_httpd.erl ---------------------------------------------------------------------- diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl index 720ea0a..eee1001 100644 --- a/src/couch_httpd.erl +++ b/src/couch_httpd.erl @@ -306,7 +306,6 @@ 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{} -> @@ -483,8 +482,7 @@ serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot, ++ couch_httpd_auth:cookie_auth_header(Req, []) ++ ExtraHeaders, ResponseHeaders1 = couch_httpd_cors:cors_headers(Req, ResponseHeaders), - ResponseHeaders2 = couch_httpd_csrf:headers(Req, ResponseHeaders1), - {ok, MochiReq:serve_file(RelativePath, DocumentRoot, ResponseHeaders2)}. + {ok, MochiReq:serve_file(RelativePath, DocumentRoot, ResponseHeaders1)}. qs_value(Req, Key) -> qs_value(Req, Key, undefined). @@ -656,8 +654,7 @@ 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), - Headers3 = couch_httpd_csrf:headers(Req, Headers2), - Resp = MochiReq:start_response_length({Code, Headers3, Length}), + Resp = MochiReq:start_response_length({Code, Headers2, Length}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -670,8 +667,7 @@ 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), - Headers3 = couch_httpd_csrf:headers(Req, Headers2), - Resp = MochiReq:start_response({Code, Headers3}), + Resp = MochiReq:start_response({Code, Headers2}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -706,8 +702,7 @@ 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), - Headers4 = couch_httpd_csrf:headers(Req, Headers3), - Resp = MochiReq:respond({Code, Headers4, chunked}), + Resp = MochiReq:respond({Code, Headers3, chunked}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -738,9 +733,8 @@ 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, Headers4, Body})}. + {ok, MochiReq:respond({Code, Headers3, 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/8c4e947e/src/couch_httpd_csrf.erl ---------------------------------------------------------------------- diff --git a/src/couch_httpd_csrf.erl b/src/couch_httpd_csrf.erl deleted file mode 100644 index 10bb175..0000000 --- a/src/couch_httpd_csrf.erl +++ /dev/null @@ -1,223 +0,0 @@ -% 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{method = 'GET'}) -> - ok; -validate(#httpd{method = 'HEAD'}) -> - ok; -validate(#httpd{method = 'OPTIONS'}) -> - ok; -validate(#httpd{} = Req) -> - Cookie = csrf_from_req(Req), - Header = couch_httpd:header_value(Req, "X-CouchDB-CSRF"), - Validate = lists:member(get_content_type(Req), csrf_mime_types()), - case {Validate, Cookie, Header} of - {false, _, _} -> - ok; % mime type is not in the list - {true, undefined, undefined} -> - throw_if_mandatory(Req); - {true, undefined, "true"} -> - throw_if_mandatory(Req); - {true, "deleted", "true"} -> - throw_if_mandatory(Req); - {true, undefined, _} -> - throw({forbidden, <<"CSRF header sent without Cookie">>}); - {true, 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. - -throw_if_mandatory(#httpd{path_parts = []}) -> - ok; %% Welcome message is public / entrypoint -throw_if_mandatory(_) -> - case csrf_mandatory() of - true -> - throw({forbidden, <<"CSRF Cookie/Header is mandatory">>}); - false -> - ok - 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("csrf", "secret", undefined) of - undefined -> - NewSecret = ?b2l(couch_uuids:random()), - config:set("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.9. - - -max_age() -> - config:get_integer("csrf", "timeout", 3600). - - -timestamp() -> - couch_httpd_auth:make_cookie_time(). - -csrf_mandatory() -> - config:get_boolean("csrf", "mandatory", false). - -get_content_type(#httpd{} = Req) -> - case couch_httpd:header_value(Req, "Content-Type") of - undefined -> - undefined; - ReqCtype -> - case string:tokens(ReqCtype, ";") of - [Ctype] -> Ctype; - [Ctype | _Rest] -> Ctype - end - end. - -csrf_mime_types() -> - Default = - "application/x-www-form-urlencoded," - "multipart/form-data," - "text/plain", - MimeTypes = config:get("csrf", "mime_types", Default), - split(MimeTypes). - -split(CSV) -> - re:split(CSV, "\\s*,\\s*", [trim, {return, list}]).