Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preventing Web API from executing AT ALL if the EnableCors Origin is invalid

I am using Microsofts EnableCors Attribute for my Web API calls. The client-side behavior runs as I would expect: e.g. the call returns as failed when the Origin is invalid.

However, when I put a break-point inside the method & call from an invalid Origin...the method still executes top-to-bottom (even though the client gets a failed result). If the Origin is invalid, I dont want it to execute AT ALL.

MY QUESTION IS:
How can I prevent the Web API method from executing AT ALL if the EnableCors Origin is invalid?

Help me Obi-Wan Kenobi...you're my only hope.

MY CODE LOOKS LIKE:

[HttpPost]
[EnableCors(origins: "www.zippitydoodah.com", headers: "*", methods: "*")]
public HttpResponseMessage Enqueue(HttpRequestMessage request)
{
    // NONE OF THIS SHOULD RUN: If the Origin is bad...but (oddly) it is
    TraceHandler.TraceIn(TraceLevel.Info);

    string claimId = string.Empty;
    ClaimMessage claimMessage = null;

    try 
    {
        claimId = GetClaimId(request);
        claimMessage = CreateClaimMessage(claimId, segmentClaimFullName);

        Enqueue(claimMessage);

        TraceHandler.TraceAppend(FORMAT_ENQUEUED_SUCCESS, claimId);
    }
    catch (Exception ex)
    {
        TraceHandler.TraceError(ex);
        TraceHandler.TraceOut();

        EnqueueToPoison(ex, claimMessage);
        return Request.CreateResponse(HttpStatusCode.InternalServerError, GetHttpError());
    }

    TraceHandler.TraceOut();
    return Request.CreateResponse(HttpStatusCode.OK, string.Format(FORMAT_ENQUEUED_SUCCESS, claimId));
}

MY CONFIG LOOKS LIKE:

public static class WebApiConfig
{
    #region <Methods>

    public static void Register(HttpConfiguration config)
    {
        // ENABLE CORS
        config.EnableCors();

        // CREATE ROUTES
        config.Routes.MapHttpRoute(
            name: "DefaultRpcApiActions",
            routeTemplate: "api/{controller}/actions/{action}/{id}",
            defaults: new { id = RouteParameter.Optional });

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional });
    }

    #endregion
}
like image 685
Prisoner ZERO Avatar asked Apr 11 '16 18:04

Prisoner ZERO


People also ask

How do I resolve cross-origin issues in Web API?

Say the front-end is served from https://demodomain-ui.com and the backend is served from from https://demodomain-service.com/api. If an end user tries to access the application, for security reasons the browsers restrict cross-origin HTTP requests initiated from the UI. The first is to install the Microsoft. AspNet.

What is CORS issue in Web API?

Cross-origin resource sharing (CORS) is a browser security feature that restricts cross-origin HTTP requests that are initiated from scripts running in the browser. If your REST API's resources receive non-simple cross-origin HTTP requests, you need to enable CORS support.

What is the originsparameter of the [enablecors] attribute?

The originsparameter of the [EnableCors]attribute specifies which origins are allowed to access the resource. The value is a comma-separated list of the allowed origins.

How to enable cors on the web API?

Enabling CORS on Web API. There are two ways by which we can enable CORS on the Web API. Adding “Access-Control-Allow-Origin” as Response Header. One way to achieve the CORS is by adding the response header for the Origin domain as shown in the below code. In the below code I am the header with the origin value by taking it our from the request.

What does the Allow Origin Access control HTTP header contain?

The allow origin access control http header returned when using this method contains the origin that sent the request, not a wildcard, e.g. Access-Control-Allow-Origin: http://localhost:4200. This is an example Startup.cs file an ASP.NET Core 3.1 API that supports CORS requests from any origin with credentials.

How to allow origin to send requests to the API?

To fix the issue and still allow any origin you can use this method instead:.SetIsOriginAllowed (origin => true). The lambda function that you pass to the.SetIsOriginAllowed () method returns true if an origin is allowed, so always returning true allows any origin to send requests to the api.


1 Answers

As it turns out...

CORS headers are not expected to PREVENT calls to a controller: MVC or Web API. It merely prevents the RESULTS from being returned to the browser. The method it will get executed no matter what...so you must prevent execution by other means.

WHAT OTHER MEANS?
You can probably do it using an AuthorizeAttribute. But I wanted to do it at the ACTION level, so I chose an ActionFilterAttribute - and performed checks in in OnActionExecuting

NOTES ON USING: The ActionFilterAttribute

  • Go ahead and open CORS to all & then restrict by action (so everyone can PING)

  • Assumes all calls come from a valid REFERRER

This means things like SQL CLR calls from a database (which is what I am doing) won't work because the REFERRER is null (because of this, I will be posting a better solution later).

  • The ICorsPolicyProvider is useless - I am removing it (but included it here)

All the examples I saw included it, but I have yet to find a scenario where it gets called. My constructor already creates the CorsPolicy & the policy is available throughout the calls lifetime...so the ICorsPolicyProvider method seems pretty useless (at the moment).

  • The TraceHandler implementation is my own - go ahead & use your own instead

  • The Access-Control-Allow-Origin header had to be added to ensure expected return-message behavior for certain clients

HERE IS THE CODE: The ActionFilterAttribute

namespace My.Application.Security
{
    using My.Application.Diagnostics;
    using System;
    using System.Configuration;
    using System.Diagnostics;
    using System.Net;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.Cors;
    using System.Web.Http.Controllers;
    using System.Web.Http.Cors;
    using System.Web.Http.Filters;

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class EnableWebApiCorsFromAppSettingsAttribute : ActionFilterAttribute, ICorsPolicyProvider
    {
        #region <Fields & Constants>

        private const string EXCEPTION_CONTEXT_NULL = "Access Denied: HttpActionContext cannot be null.";
        private const string EXCEPTION_REFERRER_NULL = "Access Denied: Referrer cannot be null.";
        private const string FORMAT_INVALID_REFERRER = "Access Denied: '{0}' is not a valid referrer.";
        private const string FORMAT_REFERRER = "Referrer: '{0}' was processed for this request.";
        private const string FORMAT_REFERRER_FOUND = "Referrer IsFound: {0}.";

        private readonly CorsPolicy policy;

        #endregion

        #region <Constructors>

        public EnableWebApiCorsFromAppSettingsAttribute(string appSettingKey, bool allowAnyHeader = true, bool allowAnyMethod = true, bool supportsCredentials = true)
        {
            policy = new CorsPolicy();
            policy.AllowAnyOrigin = false;
            policy.AllowAnyHeader = allowAnyHeader;
            policy.AllowAnyMethod = allowAnyMethod;
            policy.SupportsCredentials = supportsCredentials;

            SetValidOrigins(appSettingKey);

            if (policy.Origins.Count == 0)
                policy.AllowAnyOrigin = true;
        }

        #endregion

        #region <Methods>

        #region public

        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            TraceHandler.TraceIn(TraceLevel.Info);

            if (actionContext == null)
                throw new ArgumentNullException("HttpActionContext");

            if (actionContext.Request.Headers.Referrer == null)
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, EXCEPTION_REFERRER_NULL);

            var referrer = actionContext.Request.Headers.Referrer.ToString();

            TraceHandler.TraceAppend(string.Format(FORMAT_REFERRER, referrer));

            // If no Origins Are Set - Do Nothing
            if (policy.Origins.Count > 0)
            {
                var isFound = policy.Origins.Contains(referrer);

                TraceHandler.TraceAppend(string.Format(FORMAT_REFERRER_FOUND, isFound));

                if (!isFound)
                {
                    TraceHandler.TraceAppend("IsFound was FALSE");
                    actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, string.Format(FORMAT_INVALID_REFERRER, referrer));
                }
            }

            TraceHandler.TraceOut();
            base.OnActionExecuting(actionContext);
        }

        public Task<CorsPolicy> GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (cancellationToken.CanBeCanceled && cancellationToken.IsCancellationRequested)
                return Task.FromResult<CorsPolicy>(null);

            return Task.FromResult(policy);
        }

        #endregion

        #region private

        private void SetValidOrigins(string appSettingKey)
        {
            // APP SETTING KEY: <add key="EnableCors.Origins" value="http://www.zippitydoodah.com" />
            var origins = string.Empty;
            if (!string.IsNullOrEmpty(appSettingKey))
            {
                origins = ConfigurationManager.AppSettings[appSettingKey];
                if (!string.IsNullOrEmpty(origins))
                {
                    foreach (string origin in origins.Split(",;|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
                        policy.Origins.Add(origin);
                }
            }
        }

        #endregion

        #endregion
    }
}

HERE IS USAGE OF THE CODE: The ActionFilterAttribute

namespace My.Application.Web.Controllers
{
    using Security;
    using My.Application.Diagnostics;
    using My.Application.Framework.Configuration;
    using My.Application.Models;
    using My.Application.Process;
    using System;
    using System.Configuration;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Messaging;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;
    using System.Web.Http.Cors;
    using System.Xml.Linq;
    using System.Xml.Serialization;

    [EnableCors(origins: "*", headers: "*", methods: "*")]
    public class OutboundEventController : ApiControllerBase
    {
        #region <Actions>

        [HttpGet]
        public HttpResponseMessage Ping()
        {
            TraceHandler.TraceIn(TraceLevel.Info);

            if (Request.Headers.Referrer == null)
                TraceHandler.TraceAppend(MESSAGE_REFERRER_NULL);

            if (Request.Headers.Referrer != null)
                TraceHandler.TraceAppend(string.Format(FORMAT_REFERRER, Request.Headers.Referrer));

            TraceHandler.TraceOut();
            return Request.CreateResponse(HttpStatusCode.OK, "Ping back at cha...");
        }

        [HttpPost]
        [EnableWebApiCorsFromAppSettings("EnableCors.Origins")]
        public HttpResponseMessage Enqueue(HttpRequestMessage request)
        {
            TraceHandler.TraceIn(TraceLevel.Info);

            if (Request.Headers.Referrer == null)
                TraceHandler.TraceAppend(MESSAGE_REFERRER_NULL);

            if (Request.Headers.Referrer != null)
                TraceHandler.TraceAppend(string.Format(FORMAT_REFERRER, Request.Headers.Referrer));

            try 
            {
                // Do Amazing Stuff Here...

                TraceHandler.TraceAppend(FORMAT_ENQUEUED_SUCCESS, claimId);
            }
            catch (Exception ex)
            {
                TraceHandler.TraceError(ex);
                TraceHandler.TraceOut();

                EnqueueToPoison(ex, claimMessage);
                return Request.CreateResponse(HttpStatusCode.InternalServerError, GetHttpError());
            }

            TraceHandler.TraceOut();

            // FORCE: Correct Header
            var response = Request.CreateResponse(HttpStatusCode.OK, string.Format(FORMAT_ENQUEUED_SUCCESS, claimId));
            response.Headers.Add("Access-Control-Allow-Origin", "*");
            return response;
        }

        #endregion

        private string GetClaimId(HttpRequestMessage request)
        {
            var stream = request.Content.ReadAsStreamAsync().Result;
            var xdoc = XDocument.Load(stream);
            var result = GetElementValue(xdoc, "ClaimId");

            return result;
        }

        private ClaimMessage CreateClaimMessage(string claimId, string process)
        {
            ClaimMessage message = new ClaimMessage();

            message.ClaimID = claimId;
            message.Process = process;

            return message;
        }

        private void Enqueue(ClaimMessage claimMessage)
        {
            var queueName = ConfigurationManager.AppSettings[Settings.Messaging.Queue.Name].ToString();
            var queue = new MessageQueue(queueName);
            queue.DefaultPropertiesToSend.Recoverable = true;

            TraceHandler.TraceAppend(FORMAT_QUEUE_NAME, queueName);

            MessageQueueTransaction transaction;
            transaction = new MessageQueueTransaction();
            transaction.Begin();

            var message = new System.Messaging.Message();
            message.Formatter = new XmlMessageFormatter(new Type[] { typeof(ClaimMessage) });

            message.Label = "ClaimID " + claimMessage.ClaimID;

            message.Body = claimMessage;
            queue.Send(message, transaction);

            transaction.Commit();
            queue.Close();
        }

        private void EnqueueToPoison(Exception exception, ClaimMessage claimdata)
        {
            TraceHandler.TraceIn(TraceLevel.Info);

            var poison = ToPoisonMessage(exception, claimdata);
            var message = new System.Messaging.Message();

            try
            {
                var poisonQueueName = ConfigurationManager.AppSettings[Settings.Messaging.PoisonQueue.Name].ToString();

                TraceHandler.TraceAppend(FORMAT_QUEUE_NAME, poisonQueueName);

                if (MessageQueue.Exists(poisonQueueName))
                {
                    var queue = new MessageQueue(poisonQueueName);
                    queue.DefaultPropertiesToSend.Recoverable = true;

                    var transaction = new MessageQueueTransaction();
                    transaction.Begin();

                    message.Formatter = new XmlMessageFormatter(new Type[] { typeof(PoisonClaimMessage) });
                    message.Label = "Poison ClaimID " + poison.ClaimID;

                    var xmlSerializer = new XmlSerializer(poison.GetType());
                    xmlSerializer.Serialize(message.BodyStream, poison);

                    queue.Send(message, transaction);

                    TraceHandler.TraceAppend(FORMAT_ENQUEUED_POISON_SUCCESS, poison.ClaimID);

                    transaction.Commit();
                    queue.Close();
                }
            }
            catch(Exception ex)
            {
                // An error occurred while enqueuing to POISON
                var poisonXml = ToString(poison);

                TraceHandler.TraceError(ex);
                TraceHandler.TraceAppend(poisonXml);
            }
            finally
            {
                TraceHandler.TraceOut();
            }
        }


        #endregion
    }
}

APPLICATION SETTINGS: The ActionFilterAttribute

  <appSettings>
    <add key="EnableCors.Origins" value="" />
  </appSettings>
like image 171
Prisoner ZERO Avatar answered Oct 20 '22 08:10

Prisoner ZERO