diff --git a/winixd/core/app.cpp b/winixd/core/app.cpp index db20b54..390c7a6 100644 --- a/winixd/core/app.cpp +++ b/winixd/core/app.cpp @@ -654,8 +654,6 @@ void App::MakeRenameMeToABetterName() //////////////////////// - - cur.request->PrepareAnswerType(); if( cur.session->ip_ban && cur.session->ip_ban->IsIPBanned() ) @@ -669,6 +667,11 @@ void App::MakeRenameMeToABetterName() cur.request->http_status = Header::status_403_forbidden; } + if( cur.request->function ) + { + cur.request->function->CheckOriginHeader(); + } + // cur.request->status can be changed by function_parser if( cur.request->status == WINIX_ERR_OK && cur.request->http_status == Header::status_200_ok ) plugin.Call(WINIX_PREPARE_REQUEST); @@ -687,7 +690,9 @@ void App::MakeRenameMeToABetterName() AddDefaultModels(); - if( cur.request->status == WINIX_ERR_OK && cur.request->http_status == Header::status_200_ok ) + // what about 204 No Content from preflight requests? should we make MakeOptions()? + if( (cur.request->status == WINIX_ERR_OK && cur.request->http_status == Header::status_200_ok) || + (cur.request->method == Request::Method::options && cur.request->http_status == Header::status_204_no_content)) functions.MakeFunction(); if( cur.request->run_state == Request::RunState::normal_run ) diff --git a/winixd/core/config.cpp b/winixd/core/config.cpp index ec768c5..0aa2f65 100644 --- a/winixd/core/config.cpp +++ b/winixd/core/config.cpp @@ -353,6 +353,8 @@ void Config::AssignValues() allow_all_cors_origins = Bool(L"allow_all_cors_origins", false); ListText(L"allowed_cors_origins", allowed_cors_origins); + ListText(L"access_control_expose_headers", access_control_expose_headers); + access_control_allow_credentials = Bool(L"access_control_allow_credentials", false); } diff --git a/winixd/core/config.h b/winixd/core/config.h index 0616b3c..c0461a0 100644 --- a/winixd/core/config.h +++ b/winixd/core/config.h @@ -984,10 +984,23 @@ public: bool allow_all_cors_origins; // list of allowed origins in cors requests + // can be set per controller in a method: virtual bool FunctionBase::IsCorsOriginAvailable(const std::wstring & origin_url) // used only if allow_all_cors_origins is false // default: empty std::vector allowed_cors_origins; + // list of additional headers sent in Access-Control-Expose-Headers header + // can be set per controller in a method: virtual void FunctionBase::AddAccessControlExposeHeadersHeader() + // default: empty + std::vector access_control_expose_headers; + + // if true return Access-Control-Allow-Credentials header equal "true" + // whethert credentials (e.g. cookies, authorization headers) are available in cors requests + // can be set per controller in a method: virtual bool FunctionBase::AreCorsCredentialsAvailable() + // default: false + bool access_control_allow_credentials; + + Config(); bool ReadConfig(const std::wstring & config_file); diff --git a/winixd/core/header.cpp b/winixd/core/header.cpp index 29b29fb..2f95f5e 100644 --- a/winixd/core/header.cpp +++ b/winixd/core/header.cpp @@ -69,6 +69,47 @@ void Header::prepare_status_value(int http_status, pt::WTextStream & value, bool } +bool Header::is_header_value_char_correct(wchar_t c) +{ + /* + * make sure to not allow at least \r or \r + */ + return c > 32 && c < 127; +} + + +bool Header::is_header_value_correct(const wchar_t * str) +{ + for( ; *str ; ++str) + { + if( !is_header_value_char_correct(*str) ) + { + return false; + } + } + + return true; +} + + +bool Header::is_header_value_correct(const std::wstring & str) +{ + /* + * dont use is_header_value_correct(str.c_str()) as there can be a null character (0) inside the string + */ + for(size_t i=0 ; i < str.size() ; ++i) + { + if( !is_header_value_char_correct(str[i]) ) + { + return false; + } + } + + return true; +} + + + } diff --git a/winixd/core/header.h b/winixd/core/header.h index e59b690..6483f5b 100644 --- a/winixd/core/header.h +++ b/winixd/core/header.h @@ -54,10 +54,13 @@ public: static constexpr const wchar_t * accept_language = L"Accept-Language"; static constexpr const wchar_t * authorization = L"Authorization"; static constexpr const wchar_t * allow = L"Allow"; + static constexpr const wchar_t * very = L"Very"; + static constexpr const wchar_t * origin = L"Origin"; static constexpr const wchar_t * access_control_allow_methods = L"Access-Control-Allow-Methods"; static constexpr const wchar_t * access_control_allow_origin = L"Access-Control-Allow-Origin"; static constexpr const wchar_t * access_control_allow_headers = L"Access-Control-Allow-Headers"; static constexpr const wchar_t * access_control_allow_credentials = L"Access-Control-Allow-Credentials"; + static constexpr const wchar_t * access_control_expose_headers = L"Access-Control-Expose-Headers"; static constexpr const wchar_t * access_control_max_age = L"Access-Control-Max-Age"; /* @@ -132,6 +135,9 @@ public: static const wchar_t * find_status_string_value(int http_status); static void prepare_status_value(int http_status, pt::WTextStream & value, bool clear_value = true); + static bool is_header_value_char_correct(wchar_t c); + static bool is_header_value_correct(const wchar_t * str); + static bool is_header_value_correct(const std::wstring & str); protected: diff --git a/winixd/core/request.cpp b/winixd/core/request.cpp index fd20fbe..063b6c6 100644 --- a/winixd/core/request.cpp +++ b/winixd/core/request.cpp @@ -1531,37 +1531,10 @@ void Request::PrepareSessionCookie() } -// preflight request are tested in function->MakeOption() -void Request::CheckCorsHeaders() -{ - pt::Space * origin = headers_in.get_space_nc(L"Origin"); - - if( origin && origin->is_wstr() && function ) - { - if( !out_headers.has_key(Header::access_control_allow_origin) ) - { - if( function->IsCorsOriginAvailable(*origin->get_wstr()) ) - { - function->AddAccessControlAllowOriginHeader(*origin->get_wstr()); - } - } - - if( !out_headers.has_key(Header::access_control_allow_credentials) ) - { - if( function->AreCorsCredentialsAvailable() ) - { - function->AddAccessControlAllowCredentialsHeader(); - } - } - } -} - - void Request::PrepareHeaders(bool compressing, int compress_encoding, size_t output_size) { PrepareSessionCookie(); - CheckCorsHeaders(); if( send_as_attachment ) { diff --git a/winixd/core/request.h b/winixd/core/request.h index df38608..6eda69a 100644 --- a/winixd/core/request.h +++ b/winixd/core/request.h @@ -595,7 +595,6 @@ private: int SelectDeflateVersion(); void SelectCompression(size_t source_len, bool & compression_allowed, int & compression_encoding); void PrepareSessionCookie(); - void CheckCorsHeaders(); void PrepareHeaders(bool compressing, int compress_encoding, size_t output_size); void ModifyStatusForRedirect(); void PrepareSendFileHeaderForStaticMountpoint(); diff --git a/winixd/functions/functionbase.cpp b/winixd/functions/functionbase.cpp index 63d32ea..9fae7fc 100644 --- a/winixd/functions/functionbase.cpp +++ b/winixd/functions/functionbase.cpp @@ -142,6 +142,15 @@ bool FunctionBase::HasAccess() } +/* + * this is in a response to the normal OPTIONS method (not cors request) + */ +void FunctionBase::AddAllowMethodsHeader() +{ + cur->request->out_headers.add(Header::allow, L"GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH"); +} + + bool FunctionBase::IsCorsMethodAvailable(Request::Method method) { return method == Request::get || method == Request::head || method == Request::post || method == Request::put || @@ -172,7 +181,7 @@ bool FunctionBase::IsCorsOriginAvailable(const std::wstring & origin_url) bool FunctionBase::AreCorsCredentialsAvailable() { - return true; + return config && config->access_control_allow_credentials; } @@ -222,7 +231,10 @@ void FunctionBase::AddAccessControlAllowOriginHeader(const std::wstring & origin */ void FunctionBase::AddAccessControlAllowHeadersHeader(const std::wstring & headers) { - cur->request->AddHeader(Header::access_control_allow_headers, headers); + if( Header::is_header_value_correct(headers) ) + { + cur->request->AddHeader(Header::access_control_allow_headers, headers); + } } @@ -239,6 +251,158 @@ void FunctionBase::AddAccessControlAllowCredentialsHeader() } +void FunctionBase::AddAccessControlExposeHeadersHeader() +{ + if( config ) + { + if( !config->access_control_expose_headers.empty() ) + { + pt::WTextStream headers; + bool is_first = true; + + for(std::wstring & str : config->access_control_expose_headers) + { + if( !is_first ) + headers << ", "; + + headers << str; + is_first = false; + } + + cur->request->AddHeader(Header::access_control_expose_headers, headers); + } + } +} + + +void FunctionBase::AddCorsPreflightRequestHeaders(const std::wstring & origin, Request::Method method, const std::wstring * request_headers) +{ + AddAccessControlAllowMethodsHeader(method); + AddAccessControlAllowOriginHeader(origin); + AddAccessControlMaxAgeHeader(); + AddAccessControlExposeHeadersHeader(); + + if( AreCorsCredentialsAvailable() ) + { + AddAccessControlAllowCredentialsHeader(); + } + + if( request_headers ) + { + AddAccessControlAllowHeadersHeader(*request_headers); + } + + log << log3 << "FunctionBase: this cors request is permitted" << logend; +} + + +void FunctionBase::AddCorsNormalRequestHeaders(const std::wstring & origin) +{ + AddAccessControlAllowOriginHeader(origin); + + if( AreCorsCredentialsAvailable() ) + { + AddAccessControlAllowCredentialsHeader(); + } +} + + +void FunctionBase::AddResponseHeadersForOrigin(const std::wstring & origin) +{ + if( cur->request->method == Request::Method::options ) + { + pt::Space * cors_method = cur->request->headers_in.get_space_nc(L"Access_Control_Request_Method"); // FastCGI changes '-' to '_' + pt::Space * cors_headers = cur->request->headers_in.get_space_nc(L"Access_Control_Request_Headers"); + + if( cors_method && cors_method->is_wstr() ) + { + /* + * this is a preflight request + * https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request + * (we allow Access-Control-Request-Headers not to be present) + */ + Request::Method method = Request::CheckRequestMethod(cors_method->get_wstr()->c_str()); + cur->request->http_status = Header::status_204_no_content; + + if( IsCorsMethodAvailable(method) ) + { + bool cors_headers_available = true; + std::wstring * headers = nullptr; + + if( cors_headers && cors_headers->is_wstr() ) + { + headers = cors_headers->get_wstr(); + cors_headers_available = AreCorsHeadersAvailable(*headers); + } + + if( cors_headers_available ) + { + AddCorsPreflightRequestHeaders(origin, method, headers); + } + else + { + if( headers ) + { + log << log2 << "FunctionBase: these cors headers: " << *headers << " are not permitted in cors requests" << logend; + } + } + } + else + { + log << log2 << "FunctionBase: this method: " << *cors_method->get_wstr() << " is not permitted in cors requests" << logend; + } + } + else + { + /* + * this is not a preflight cors request + */ + AddAllowMethodsHeader(); + AddCorsNormalRequestHeaders(origin); + } + } + else + { + AddCorsNormalRequestHeaders(origin); + } +} + + +void FunctionBase::CheckOriginHeader() +{ + pt::Space * origin = cur->request->headers_in.get_space_nc(L"Origin"); + + if( origin && origin->is_wstr() ) + { + if( IsCorsOriginAvailable(*origin->get_wstr()) ) + { + AddResponseHeadersForOrigin(*origin->get_wstr()); + } + else + { + cur->request->http_status = Header::status_204_no_content; + log << log2 << "FunctionBase: this origin: " << *origin->get_wstr() << " is not permitted for cors requests" << logend; + } + + /* + * https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties + * https://security.stackexchange.com/questions/151590/vary-origin-response-header-and-cors-exploitation + * It's important to include the Vary: Origin header to prevent caching. The header indicates that the response + * is in some way dependent on the origin and should therefore not be served from cache for any other origin. + * If the header is missing, cache poisoning attacks might be possible + */ + cur->request->AddHeader(Header::very, Header::origin); + } + else + { + if( cur->request->method == Request::Method::options ) + { + AddAllowMethodsHeader(); + } + } +} + + void FunctionBase::MakeGet() { @@ -247,7 +411,7 @@ void FunctionBase::MakeGet() void FunctionBase::MakeHead() { - // by default call MakeGet() but do not return any content at the end of the request + // by default call MakeGet() but we do not return any content at the end of the request MakeGet(); } @@ -271,65 +435,11 @@ void FunctionBase::MakeConnect() // do nothing by default } - void FunctionBase::MakeOptions() { - cur->request->http_status = Header::status_204_no_content; - - pt::Space * cors_method = cur->request->headers_in.get_space_nc(L"Access_Control_Request_Method"); // FastCGI changes '-' to '_' - pt::Space * cors_headers = cur->request->headers_in.get_space_nc(L"Access_Control_Request_Headers"); - pt::Space * cors_origin = cur->request->headers_in.get_space_nc(L"Origin"); - - if( cors_method && cors_origin && cors_method->is_wstr() && cors_origin->is_wstr() ) - { - /* - * this is a preflight request - * https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request - * (we allow Access-Control-Request-Headers not to be present) - */ - Request::Method method = Request::CheckRequestMethod(cors_method->get_wstr()->c_str()); - bool cors_available = false; - - if( IsCorsMethodAvailable(method) && IsCorsOriginAvailable(*cors_origin->get_wstr()) ) - { - cors_available = true; - - if( cors_headers && cors_headers->is_wstr() ) - { - cors_available = AreCorsHeadersAvailable(*cors_headers->get_wstr()); - } - } - - if( cors_available ) - { - AddAccessControlAllowMethodsHeader(method); - AddAccessControlAllowOriginHeader(*cors_origin->get_wstr()); - AddAccessControlMaxAgeHeader(); - - if( AreCorsCredentialsAvailable() ) - { - AddAccessControlAllowCredentialsHeader(); - } - - if( cors_headers && cors_headers->is_wstr() ) - { - AddAccessControlAllowHeadersHeader(*cors_headers->get_wstr()); - } - - log << log3 << "FunctionBase: cors requests are permitted" << logend; - } - else - { - log << log2 << "FunctionBase: cors requests are not permitted" << logend; - } - } - else - { - cur->request->out_headers.add(Header::allow, L"GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH"); - } + // do nothing by default } - void FunctionBase::MakeTrace() { // do nothing by default diff --git a/winixd/functions/functionbase.h b/winixd/functions/functionbase.h index 3da2bf7..16e96fa 100644 --- a/winixd/functions/functionbase.h +++ b/winixd/functions/functionbase.h @@ -111,6 +111,8 @@ public: virtual bool HasAccess(); + virtual void AddAllowMethodsHeader(); + virtual bool IsCorsMethodAvailable(Request::Method method); virtual bool IsCorsOriginAvailable(const std::wstring & origin_url); virtual bool AreCorsCredentialsAvailable(); @@ -121,6 +123,11 @@ public: virtual void AddAccessControlAllowHeadersHeader(const std::wstring & headers); virtual void AddAccessControlMaxAgeHeader(); virtual void AddAccessControlAllowCredentialsHeader(); + virtual void AddAccessControlExposeHeadersHeader(); + virtual void AddCorsPreflightRequestHeaders(const std::wstring & origin, Request::Method method, const std::wstring * request_headers); + virtual void AddCorsNormalRequestHeaders(const std::wstring & origin); + virtual void AddResponseHeadersForOrigin(const std::wstring & origin); + virtual void CheckOriginHeader(); virtual void MakeGet();