Adding Referrer Protection to OpenWhisk Actions

Today I was thinking about what it would take to add referrer-style protection to an OpenWhisk API. What I mean by that is the ability to say that a particular API can only be called from certain domains. To be clear, this is not secure in any "real" fashion. This Stack Overflow question does a great job of addressing why not: "Does referrer header checking offer any real world security improvement?" However, I do think it can help prevent some misuse, and perhaps even help prevent accidental versus malicious misuse. As long as you keep in mind that this is minimally protective, then I think you will be fine. Also note that you should look into the API Gateway feature for more ways to lock down your APIs. Ok, with that out of the way, let's look at how this could be implemented.

First off - when an OpenWhisk action is enabled for web usage, you automatically get access to multiple different aspects of the request. The docs cover this in the "HTTP Context" section. The crucial bit is under __ow_headers where you get access to all of the HTTP headers used for the request. To help illustrate this, I built a simple "echo" action that looks like so:

function main(args) {

    return { arguments: args };

}

I then enabled it as a web action and built a web page to call it:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Page Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

    <pre id="result">

    </pre>

    <script>
    document.addEventListener('DOMContentLoaded', init);
    function init() {
        fetch('https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/safeToDelete/webecho.json')
        .then(res => res.json())
        .then(res => {
            document.querySelector('#result').innerHTML = JSON.stringify(res,null, '\t');
        });
    }
    </script>

</body>
</html>

Basically - on page load, call the API and dump the results into a div. I then used Surge.sh to host the static file. You can see this here: http://grieving-skate.surge.sh/temp.html Note - like most of my demos of this nature, I cannot promise the host/API will be up forever. So with that in mind, here is what the output looks like:

{
    "arguments": {
        "__ow_method": "get",
        "__ow_headers": {
            "accept": "*/*",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
            "x-client-ip": "76.72.12.163",
            "accept-language": "en-US, en;q=0.9",
            "x-forwarded-proto": "https",
            "host": "openwhisk.ng.bluemix.net:443",
            "cache-control": "no-transform",
            "origin": "http://grieving-skate.surge.sh",
            "via": "1.1 CQAAAI+qTCs-",
            "x-global-transaction-id": "3840698005",
            "referer": "http://grieving-skate.surge.sh/temp.html",
            "accept-encoding": "gzip, deflate, br",
            "x-forwarded-for": "76.72.12.163"
        },
        "__ow_path": ""
    }
}

As you can see, under __ow_headers.referer I have access to the page where the API was used. Ok, so let's try using this in a semi-real action:

const allowedRef = ['grieving-skate.surge.sh'];

function main(args) {

    if(safeCaller(args.__ow_headers.referer)) {
        return { arguments: args };
    } else {
        throw new Error('Invalid Referer');
    }
}

function safeCaller(referrer) {
    let ok = false;
    allowedRef.forEach(ref => {
        if(referrer.indexOf(ref) >= 0) ok = true;
    });
    return ok;
}

On top I've added a new array called allowedRef. This will contain a list of hostnames allowed to use the API. I then built a new function, safeCaller, that checks the current referrer value to see if it matches any of the domains. If yes, everything carries on. If not, we return false and the action throws an error. I put this up on Surge in another domain - http://level-glove.surge.sh/temp.html. If you run that, the output will be:

{
    "error": "There was an error processing your request.",
    "code": 14186297
}

Woot. Ok, but I don't like messing up my original action (as simple as it was) with unrelated logic, so let's break this apart. Now - at this point I went ahead and built a "generic" referrer checker that could be combined with an action sequence. My first demo used arguments to let you specify the domains that were allowed to run the sequence. You may think that specifying allowed domains in an parameter is risky, but default parameters specified for web actions are set as protected and can't be overridden.

However, you can't specify default parameters for sequences. There is an open bug for this (https://github.com/apache/incubator-openwhisk/issues/2008) and there is a workaround for packages, but in that case, the parameter isn't protected. Unfortunately, we have to hard code the domains.

First, I'll create the referrer checker.

const allowedRef = ['grieving-skate.surge.sh'];

function main(args) {

    let referrer = args.__ow_headers.referer;
    let ok = false;
    allowedRef.forEach(ref => {
        if(referrer.indexOf(ref) >= 0) ok = true;
    });

    if(ok) {
        return { args:args };
    } else {
        throw new Error('Invalid Referrer');
    }

}

I've taken the code from the previous action and simply moved it into a new action. For the reasons I stated above I still have to include the allowed referrers in my code, but I can live with it. On success, I simply pass along the original arguments that were sent, and on failure, I then throw an error. I built this as a new action sequence:

wsk action update safeToDelete/webecho2 --sequence safeToDelete/checkReferrer,safeToDelete/webecho --web true

I updated both of my demos on Surge with the new URLs and confirmed they both still work correctly.

So all in all, rather simple, although remember the warnings up top - this isn't going to be very secure and should only be used for a 'casual' check of where the API is being used. But what do you think?

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also