couchdb (0.11.0-2.3) CVE-2010-2234.patch

Summary

 share/www/script/couch.js           |    2 +
 share/www/script/test/basics.js     |    9 ++++++--
 share/www/script/test/batch_save.js |    5 +++-
 share/www/script/test/stats.js      |    5 +++-
 src/couchdb/couch_httpd.erl         |   39 ++++++++++++++++++++++++++++++++----
 src/couchdb/couch_httpd_auth.erl    |    1 
 src/couchdb/couch_httpd_db.erl      |   14 ++++++------
 src/couchdb/couch_httpd_show.erl    |   21 ++-----------------
 src/couchdb/couch_rep.erl           |    8 ++++---
 src/couchdb/couch_rep_writer.erl    |    3 +-
 src/couchdb/couch_util.erl          |   14 ++++++++++++
 11 files changed, 84 insertions(+), 37 deletions(-)

    
download this patch

Patch contents

diff --git a/share/www/script/couch.js b/share/www/script/couch.js
index c549542..fc4b80d 100644
--- a/share/www/script/couch.js
+++ b/share/www/script/couch.js
@@ -411,6 +411,8 @@ CouchDB.newXhr = function() {
 
 CouchDB.request = function(method, uri, options) {
   options = options || {};
+  options.headers = options.headers || {};
+  options.headers["Content-Type"] = options.headers["Content-Type"] || "application/json";
   var req = CouchDB.newXhr();
   if(uri.substr(0, "http://".length) != "http://") {
     uri = CouchDB.urlPrefix + uri
diff --git a/share/www/script/test/basics.js b/share/www/script/test/basics.js
index 0f9ac44..6a3ae47 100644
--- a/share/www/script/test/basics.js
+++ b/share/www/script/test/basics.js
@@ -152,7 +152,8 @@ couchTests.basics = function(debug) {
 
   // test that the POST response has a Location header
   var xhr = CouchDB.request("POST", "/test_suite_db", {
-    body: JSON.stringify({"foo":"bar"})
+    body: JSON.stringify({"foo":"bar"}),
+    headers: {"Content-Type": "application/json"}
   });
   var resp = JSON.parse(xhr.responseText);
   T(resp.ok);
@@ -164,6 +165,7 @@ couchTests.basics = function(debug) {
 
   // test that that POST's with an _id aren't overriden with a UUID.
   var xhr = CouchDB.request("POST", "/test_suite_db", {
+    headers: {"Content-Type": "application/json"},
     body: JSON.stringify({"_id": "oppossum", "yar": "matey"})
   });
   var resp = JSON.parse(xhr.responseText);
@@ -202,7 +204,10 @@ couchTests.basics = function(debug) {
     result = JSON.parse(xhr.responseText);
     T(result.error == "doc_validation");
 
-    xhr = CouchDB.request("POST", "/test_suite_db/", {body: data});
+    xhr = CouchDB.request("POST", "/test_suite_db/", {
+      headers: {"Content-Type": "application/json"},
+      body: data
+    });
     T(xhr.status == 500);
     result = JSON.parse(xhr.responseText);
     T(result.error == "doc_validation");
diff --git a/share/www/script/test/batch_save.js b/share/www/script/test/batch_save.js
index 1c8a2be..a1b0019 100644
--- a/share/www/script/test/batch_save.js
+++ b/share/www/script/test/batch_save.js
@@ -36,7 +36,10 @@ couchTests.batch_save = function(debug) {
 
   // repeat the tests for POST
   for(i=0; i < 100; i++) {
-    var resp = db.request("POST", db.uri + "?batch=ok", {body: JSON.stringify({a:1})});
+    var resp = db.request("POST", db.uri + "?batch=ok", {
+      headers: {"Content-Type": "application/json"},
+      body: JSON.stringify({a:1})
+    });
     T(JSON.parse(resp.responseText).ok);
   }
   
diff --git a/share/www/script/test/stats.js b/share/www/script/test/stats.js
index 793b390..c605f27 100644
--- a/share/www/script/test/stats.js
+++ b/share/www/script/test/stats.js
@@ -162,7 +162,10 @@ couchTests.stats = function(debug) {
   
   runTest("couchdb", "database_writes", {
     run: function(db) {
-      CouchDB.request("POST", "/test_suite_db", {body: '{"a": "1"}'})
+      CouchDB.request("POST", "/test_suite_db", {
+        headers: {"Content-Type": "application/json"},
+        body: '{"a": "1"}'
+      })
     },
     test: function(before, after) {
       TEquals(before+1, after, "POST'ing new docs increments doc writes.");
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index b25242f..4e13b6c 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -25,7 +25,7 @@
 -export([start_json_response/2, start_json_response/3, end_json_response/1]).
 -export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]).
 -export([send_json/2,send_json/3,send_json/4,last_chunk/1,parse_multipart_request/3]).
--export([accepted_encodings/1,handle_request_int/5]).
+-export([accepted_encodings/1,handle_request_int/5,validate_referer/1,validate_ctype/2]).
 
 start_link() ->
     % read config and register for configuration changes
@@ -294,6 +294,34 @@ increment_method_stats(Method) ->
     couch_stats_collector:increment({httpd_request_methods, Method}).
 
 
+validate_referer(Req) ->
+    Host = host_for_request(Req),
+    Referer = header_value(Req, "Referer", fail),
+    case Referer of
+    fail ->
+        throw({bad_request, <<"Referer header required.">>});
+    Referer ->
+        {_,RefererHost,_,_,_} = mochiweb_util:urlsplit(Referer),
+        if
+            RefererHost =:= Host -> ok;
+            true -> throw({bad_request, <<"Referer header must match host.">>})
+        end
+    end.
+
+validate_ctype(Req, Ctype) ->
+    case couch_httpd:header_value(Req, "Content-Type") of
+    undefined ->
+        throw({bad_ctype, "Content-Type must be "++Ctype});
+    ReqCtype ->
+        % ?LOG_ERROR("Ctype ~p ReqCtype ~p",[Ctype,ReqCtype]),
+        case re:split(ReqCtype, ";", [{return, list}]) of
+        [Ctype] -> ok;
+        [Ctype, _Rest] -> ok;
+        _Else ->
+            throw({bad_ctype, "Content-Type must be "++Ctype})
+        end
+    end.
+
 % Utilities
 
 partition(Path) ->
@@ -340,9 +368,9 @@ qs(#httpd{mochi_req=MochiReq}) ->
 path(#httpd{mochi_req=MochiReq}) ->
     MochiReq:get(path).
 
-absolute_uri(#httpd{mochi_req=MochiReq}, Path) ->
+host_for_request(#httpd{mochi_req=MochiReq}) ->
     XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"),
-    Host = case MochiReq:get_header_value(XHost) of
+    case MochiReq:get_header_value(XHost) of
         undefined ->
             case MochiReq:get_header_value("Host") of
                 undefined ->    
@@ -352,7 +380,10 @@ absolute_uri(#httpd{mochi_req=MochiReq}, Path) ->
                     Value1
             end;
         Value -> Value
-    end,
+    end.
+
+absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) ->
+    Host = host_for_request(Req),
     XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"),
     Scheme = case MochiReq:get_header_value(XSsl) of
         "on" -> "https";
diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl
index 3e2981a..54b578c 100644
--- a/src/couchdb/couch_httpd_auth.erl
+++ b/src/couchdb/couch_httpd_auth.erl
@@ -395,6 +395,7 @@ ensure_cookie_auth_secret() ->
 handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
     ReqBody = MochiReq:recv_body(),
     Form = case MochiReq:get_primary_header_value("content-type") of
+        % content type should be json
         "application/x-www-form-urlencoded" ++ _ ->
             mochiweb_util:parse_qs(ReqBody);
         _ ->
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
index 666ecb2..b7e0e8f 100644
--- a/src/couchdb/couch_httpd_db.erl
+++ b/src/couchdb/couch_httpd_db.erl
@@ -112,6 +112,7 @@ handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) ->
     send_method_not_allowed(Req, "GET,HEAD").
 
 handle_compact_req(#httpd{method='POST',path_parts=[DbName,_,Id|_]}=Req, _Db) ->
+    couch_httpd:validate_ctype(Req, "application/json"),
     ok = couch_view_compactor:start_compact(DbName, Id),
     send_json(Req, 202, {[{ok, true}]});
 
@@ -214,6 +215,7 @@ db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->
     send_json(Req, {DbInfo});
 
 db_req(#httpd{method='POST',path_parts=[DbName]}=Req, Db) ->
+    couch_httpd:validate_ctype(Req, "application/json"),
     Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
     Doc2 = case Doc#doc.id of
         <<"">> ->
@@ -282,6 +284,7 @@ db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) ->
 
 db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
     couch_stats_collector:increment({httpd, bulk_requests}),
+    couch_httpd:validate_ctype(Req, "application/json"),
     {JsonProps} = couch_httpd:json_body_obj(Req),
     DocsArray = proplists:get_value(<<"docs">>, JsonProps),
     case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of
@@ -343,6 +346,7 @@ db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
     send_method_not_allowed(Req, "POST");
 
 db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) ->
+    couch_httpd:validate_ctype(Req, "application/json"),
     {IdsRevs} = couch_httpd:json_body_obj(Req),
     IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs],
 
@@ -387,7 +391,6 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) ->
 db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
     send_method_not_allowed(Req, "POST");
 
-
 db_req(#httpd{method='POST',path_parts=[_,<<"_revs_diff">>]}=Req, Db) ->
     {JsonDocIdRevs} = couch_httpd:json_body_obj(Req),
     JsonDocIdRevs2 = 
@@ -598,14 +601,11 @@ db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
         end_json_response(Resp)
     end;
 
+
 db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
+    couch_httpd:validate_referer(Req),
     couch_doc:validate_docid(DocId),
-    case couch_httpd:header_value(Req, "Content-Type") of
-    "multipart/form-data" ++  _Rest ->
-        ok;
-    _Else ->
-        throw({bad_ctype, <<"Invalid Content-Type header for form upload">>})
-    end,
+    couch_httpd:validate_ctype(Req, "multipart/form-data"),
     Form = couch_httpd:parse_form(Req),
     case proplists:is_defined("_doc", Form) of
     true ->
diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl
index 72c6bae..5ff97b4 100644
--- a/src/couchdb/couch_httpd_show.erl
+++ b/src/couchdb/couch_httpd_show.erl
@@ -140,7 +140,7 @@ send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) ->
             Code = 200,
             ok
     end,
-    JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp),
+    JsonResp2 = couch_util:json_apply_field({<<"code">>, Code}, JsonResp),
     % todo set location field
     couch_httpd_external:send_external_response(Req, JsonResp2).
 
@@ -368,21 +368,6 @@ render_head_for_empty_list(StartListRespFun, Req, Etag, null) ->
 render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows) ->
     StartListRespFun(Req, Etag, TotalRows, null, []).
 
-
-% Maybe this is in the proplists API
-% todo move to couch_util
-json_apply_field(H, {L}) ->
-    json_apply_field(H, L, []).
-json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) ->
-    % drop matching keys
-    json_apply_field({Key, NewValue}, Headers, Acc);
-json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) ->
-    % something else is next, leave it alone.
-    json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
-json_apply_field({Key, NewValue}, [], Acc) ->
-    % end of list, add ours
-    {[{Key, NewValue}|Acc]}.
-
 apply_etag({ExternalResponse}, CurrentEtag) ->
     % Here we embark on the delicate task of replacing or creating the
     % headers on the JsonResponse object. We need to control the Etag and
@@ -396,8 +381,8 @@ apply_etag({ExternalResponse}, CurrentEtag) ->
     JsonHeaders ->
         {[case Field of
         {<<"headers">>, JsonHeaders} -> % add our headers
-            JsonHeadersEtagged = json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders),
-            JsonHeadersVaried = json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
+            JsonHeadersEtagged = couch_util:json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders),
+            JsonHeadersVaried = couch_util:json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
             {<<"headers">>, JsonHeadersVaried};
         _ -> % skip non-header fields
             Field
diff --git a/src/couchdb/couch_rep.erl b/src/couchdb/couch_rep.erl
index b7f8790..ca27747 100644
--- a/src/couchdb/couch_rep.erl
+++ b/src/couchdb/couch_rep.erl
@@ -647,10 +647,11 @@ commit_to_both(Source, Target, RequiredSeq) ->
     end,
     {SourceStartTime, TargetStartTime}.
     
-ensure_full_commit(#http_db{} = Target) ->
+ensure_full_commit(#http_db{headers = Headers} = Target) ->
     Req = Target#http_db{
         resource = "_ensure_full_commit",
         method = post,
+	headers = [{"content-type", "application/json"} | Headers],
         body = true
     },
     {ResultProps} = couch_rep_httpc:request(Req),
@@ -672,12 +673,13 @@ ensure_full_commit(Target) ->
         InstanceStartTime
     end.
 
-ensure_full_commit(#http_db{} = Source, RequiredSeq) ->
+ensure_full_commit(#http_db{headers = Headers} = Source, RequiredSeq) ->
     Req = Source#http_db{
         resource = "_ensure_full_commit",
         method = post,
         body = true,
-        qs = [{seq, RequiredSeq}]
+        qs = [{seq, RequiredSeq}],
+	headers = [{"content-type", "application/json"} | Headers]
     },
     {ResultProps} = couch_rep_httpc:request(Req),
     case proplists:get_value(<<"ok">>, ResultProps) of
diff --git a/src/couchdb/couch_rep_writer.erl b/src/couchdb/couch_rep_writer.erl
index 269b979..8b7e4fc 100644
--- a/src/couchdb/couch_rep_writer.erl
+++ b/src/couchdb/couch_rep_writer.erl
@@ -57,8 +57,9 @@ write_docs(#http_db{headers = Headers} = Db, Docs) ->
         resource = "_bulk_docs",
         method = post,
         body = {[{new_edits, false}, {docs, JsonDocs}]},
-        headers = [{"x-couch-full-commit", "false"} | Headers]
+        headers = couch_util:proplist_apply_field({"Content-Type", "application/json"}, [{"X-Couch-Full-Commit", "false"} | Headers])
     },
+    ?LOG_ERROR("headers ~p",[Request#http_db.headers]),
     ErrorsJson = case couch_rep_httpc:request(Request) of
     {FailProps} ->
         exit({target_error, proplists:get_value(<<"error">>, FailProps)});
diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl
index 311bc88..7738d72 100644
--- a/src/couchdb/couch_util.erl
+++ b/src/couchdb/couch_util.erl
@@ -19,6 +19,7 @@
 -export([encodeBase64/1, decodeBase64/1, encodeBase64Url/1, decodeBase64Url/1,
     to_hex/1,parse_term/1, dict_find/3]).
 -export([file_read_size/1, get_nested_json_value/2, json_user_ctx/1]).
+-export([proplist_apply_field/2, json_apply_field/2]).
 -export([to_binary/1, to_integer/1, to_list/1, url_encode/1]).
 -export([json_encode/1, json_decode/1]).
 -export([verify/2]).
@@ -109,6 +110,19 @@ get_nested_json_value(Value, []) ->
 get_nested_json_value(_NotJSONObj, _) ->
     throw({not_found, json_mismatch}).
 
+proplist_apply_field(H, L) ->
+    {R} = json_apply_field(H, {L}),
+    R.
+
+json_apply_field(H, {L}) ->
+    json_apply_field(H, L, []).
+json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) ->
+    json_apply_field({Key, NewValue}, Headers, Acc);
+json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) ->
+    json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
+json_apply_field({Key, NewValue}, [], Acc) ->
+    {[{Key, NewValue}|Acc]}.
+
 json_user_ctx(#db{name=DbName, user_ctx=Ctx}) ->
     {[{<<"db">>, DbName},
             {<<"name">>,Ctx#user_ctx.name},