1
votes

I have a Web API 2 project from VS 2013, using the 5.0.0.0 DLLs from Nuget (very recently dloaded).

I also created a custom CorsPolicy that limits origins. This seems to work fine, and I followed the directions here: http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api

What I've noticed with Fiddler is that, while the OPTIONS verb is properly blocked with a 400 Bad Request, the POST verb is passed straight through to the controller, and then the CorsPolicy is invoked, but by that time, the Post action has succeeded, and the client gets a 200 OK return.

I am expecting the POST to be blocked with a 400 Bad Request as well as the OPTION verb. If I understand the CORS TR correctly, it should be blocked.

Here's the diagnostic tracing from Web API from a single POST, where I have used Fiddler to set the Origin header to http://localhose. Bear in mind -- the exact same scenario correctly gets blocked with the OPTION verb.

w3wp.exe Information: 0 : Request, Method=POST, Url=http://myhost/myvdir/api/v1/MyCntrllr/MyAction, Message='http://myhost/myvdir/api/v1/MyCntrllr/MyAction'
w3wp.exe Information: 0 : Message='MyCntrllr', Operation=DefaultHttpControllerSelector.SelectController
w3wp.exe Information: 0 : Message='MyNamespace.Controllers.MyCntrllrController', Operation=DefaultHttpControllerActivator.Create
w3wp.exe Information: 0 : Message='MyNamespace.Controllers.MyCntrllrController', Operation=HttpControllerDescriptor.CreateController
w3wp.exe Information: 0 : Message='Selected action 'MyAction(Submission submission)'', Operation=ApiControllerActionSelector.SelectAction
w3wp.exe Information: 0 : Message='Value read='{ MyValue1: 24000, MyValue2: 2, MyValues3: 24,34, MyValue4: 0, MyValue5: 90001, MyValue6: c0, MyValue7: 16 }'', Operation=JsonMediaTypeFormatter.ReadFromStreamAsync
w3wp.exe Information: 0 : Message='Parameter 'submission' bound to the value '{ MyValue1: 24000, MyValue2: 2, MyValues3: 24,34, MyValue4: 0, MyValue5: 90001, MyValue6: c0, MyValue7: 16 }'', Operation=FormatterParameterBinding.ExecuteBindingAsync
w3wp.exe Information: 0 : Message='Model state is valid. Values: submission={ MyValue1: 24000, MyValue2: 2, MyValues3: 24,34, MyValue4: 0, MyValue5: 90001, MyValue6: c0, MyValue7: 16 }', Operation=HttpActionBinding.ExecuteBindingAsync
w3wp.exe Information: 0 : Message='Action returned 'MyNamespace.MyConclusion'', Operation=ReflectedHttpActionDescriptor.ExecuteAsync
w3wp.exe Information: 0 : Message='Will use same 'JsonMediaTypeFormatter' formatter', Operation=JsonMediaTypeFormatter.GetPerRequestFormatterInstance
w3wp.exe Information: 0 : Message='Selected formatter='JsonMediaTypeFormatter', content-type='application/json; charset=utf-8'', Operation=DefaultContentNegotiator.Negotiate
w3wp.exe Information: 0 : Operation=ApiControllerActionInvoker.InvokeActionAsync, Status=200 (OK)
w3wp.exe Information: 0 : Operation=MyCntrllrController.ExecuteAsync, Status=200 (OK)
w3wp.exe Information: 0 : Message='CorsPolicyProvider selected: 'MyNamespace.WhiteListOriginPolicyProvider'', Operation=CorsPolicyProviderFactory.GetCorsPolicyProvider
w3wp.exe Information: 0 : Message='CorsPolicy selected: 'AllowAnyHeader: True, AllowAnyMethod: False, AllowAnyOrigin: False, PreflightMaxAge: null, SupportsCredentials: False, Origins: {https://www.example.com,http://localhost:22221}, Methods: {POST,OPTIONS}, Headers: {}, ExposedHeaders: {}'', Operation=WhiteListOriginPolicyProvider.GetCorsPolicyAsync
w3wp.exe Information: 0 : Message='CorsResult returned: 'IsValid: False, AllowCredentials: False, PreflightMaxAge: null, AllowOrigin: , AllowExposedHeaders: {}, AllowHeaders: {}, AllowMethods: {}, ErrorMessages: {The origin 'http://localhose' is not allowed.}'', Operation=CorsEngine.EvaluatePolicy
w3wp.exe Information: 0 : Operation=CorsMessageHandler.SendAsync, Status=200 (OK)
w3wp.exe Information: 0 : Response, Status=200 (OK), Method=POST, Url=http://myhost/myvdir/api/v1/MyCntrllr/MyAction, Message='Content-type='application/json; charset=utf-8', content-length=unknown'
w3wp.exe Information: 0 : Operation=JsonMediaTypeFormatter.WriteToStreamAsync
w3wp.exe Information: 0 : Operation=MyCntrllrController.Dispose

Code:

WhiteListOriginPolicy

public class WhiteListOriginPolicy 
    : CorsPolicy
{
    public WhiteListOriginPolicy()
    {
        AllowAnyHeader = true;
        AllowAnyMethod = false;
        Methods.Add(HttpMethod.Post.ToString());
        Methods.Add(HttpMethod.Options.ToString());
        foreach (var origin in Settings.Default.WhiteListOrigins)
        {
            Origins.Add(origin);
        }
    }
}

WhiteListOrigins is a StringCollection from the web.config file

WhiteListOriginPolicyProvider

public class WhiteListOriginPolicyProvider 
    : ICorsPolicyProvider
{
    public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult((CorsPolicy) new WhiteListOriginPolicy());
    }
}

CorsPolicyProviderFactory

public class CorsPolicyProviderFactory
    : ICorsPolicyProviderFactory
{
    private readonly ICorsPolicyProvider _whiteListOriginsPolicyProvider = new WhiteListOriginPolicyProvider();

    public ICorsPolicyProvider GetCorsPolicyProvider(HttpRequestMessage request)
    {
        return _whiteListOriginsPolicyProvider;
    }
}

WebApiConfig

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        config.EnableSystemDiagnosticsTracing();
        config.SetCorsPolicyProviderFactory(new CorsPolicyProviderFactory());
        config.EnableCors();
        // Web API routes
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}",
            defaults: new { action = RouteParameter.Optional }
            );
    }
}

WhiteListOriginPolicyAttribute

I think this is redundant, but it doesn't matter whether I use this or not

public class WhiteListOriginPolicyAttribute
    : Attribute
    , ICorsPolicyProvider
{
    public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult((CorsPolicy) new WhiteListOriginPolicy());
    }
}

MyCntrllrController

[RoutePrefix("api/v1/MyCntrllr")]
public class MyCntrllrController
    : ApiController
{
    [HttpPost]
    [HttpOptions]
    [WhiteListOriginPolicy]
    [Route("MyAction")]
    public IMyConclusion MyAction([FromBody] Submission submission)
    {
        if (Request.Method == HttpMethod.Options)
        {
            return null;
        }
        if (null == submission)
        {
            var response = new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent("Request content body does not contain recognizable Submission data")
            };
            throw new HttpResponseException(response);
        }
        var engine = new CalcEngine(new DataLocatorService());
        var eligibility = engine.GetEligibility(submission);
        return eligibility;
    }
}

The OPTIONS verb in action.

Request

OPTIONS http://myhost/myvdir/api/v1/MyCntrller/MyAction HTTP/1.1
Accept: */*
Origin: http://localhose
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, accept
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: myhost
Content-Length: 0
DNT: 1
Connection: Keep-Alive
Pragma: no-cache

Response

HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 20 Nov 2013 20:32:03 GMT
Content-Length: 59

{"Message":"The origin 'http://localhose' is not allowed."}

The POST verb in action.

Request

POST http://myhost/myvdir/api/v1/MyCntrller/MyAction HTTP/1.1
Accept: */*
Origin: http://localhose
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, accept
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: myhost
Content-Length: 121
DNT: 1
Connection: Keep-Alive
Pragma: no-cache
Content-Type: application/json

{"myvalue1":24000,"myvalue2":"2","myvalues3":["24","34"],"myvalue4":0,"myvalue5":"90001","myvalue6":"c0","myvalue7":"16"}

Response

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 20 Nov 2013 20:32:33 GMT
Content-Length: 460

_omitted_
1
This feels like a workaround, but what I did to stop the POST was to add a custom AuthorizationFilterAttribute that explicitly extracts the Origin header from the Request, compares it to the whitelist origin list, and issues a BadRequest Response if it isn't in the list. - Alan McBee

1 Answers

4
votes

This is not the purpose of CORS. Browsers prevent cross origin Ajax calls, so CORS will allow the target server to relax those rules. By default the browser prevents the calling JS from getting the results of the POST, but it will not block the POST (or GET) to the endpoint. You need to implement standard authorization approaches to ensure only allowed clients can request the endpoint.