Super Secure Make Webhooks

@ScottWorld’s article about Make webhooks triggered an inquisitiveness about securing webhooks in Make. You all know I’m not a fan of excessive dependencies on glue factories, so today, I set aside my religion to explore the science of defending your Make webhooks from potential calamity.

This document establishes a practical, secure Make webhook design pattern. I’m not a security expert by any stretch. Feel free to point out flaws or improvements because security is important and these glue factories increase the exposure. :wink:

Terminology:

  • The system calling a webhook is the “webhook broadcaster” or “webhook caller”.
  • The system hosting the webhook is the “webhook listener” or “webhook receiver”.
  • A webhook transaction is also known as a “webhook event”.

General Automation Objective

  • Airtable event (record change, button push, new item in view) triggers a call to a Make webhook in the context of a given Airtable record.
  • Make receives the call and performs the webhook scenario based on the Airtable record ID passed from Airtable.
  • The Make scenario authenticates with Airtable to retrieve the designated record details to effectuate the automation task.
  • Automation tasks include but are not limited to, modifying the Airtable record or sharing the record’s data with another third-party application.
  • Performing automation interchanges over HTTP using webooks as securely as possible.

Webhook Security

By default, Make webhooks are open and accessible by any application through an HTTPS endpoint defined by the Make webhook scenario.


Example: Make Webhook Scenario

Make webhooks are fundamentally unsecured. Through a variety of approaches, Make webhooks can be better guarded.

Standard approaches include:

  • Over HTTPS - the payload of data sent by the webhook caller is encrypted, ensuring that it is very difficult to sniff communication packets by third parties. HTTPS is enabled by default; for most systems, any attempt to use HTTP (without encryption) is impossible.

  • IP Whitelisting - the webhook calling system’s IP address is identified in the webhook listener. Any webhook calls, except from IP addresses identified, are rejected.

  • Header Secret - the webhook caller includes in the communications header a secret token that the webhook listener looks for. Any webhook calls without that secret are rejected.

Advanced approaches include:

  • Hash-based Message Authentication Code (HMAC) - SHA-256 bit encryption as a hash and signed with the message body.

  • Dynamic HMAC (dHMAC) - SHA-256 bit encryption as a hash and signed with the message body and additional dynamic values such as a GUID and/or a transaction date/time.

  • Dynamic Header Secret Rotation (DHSR) - header secrets that change periodically through automation in a zero-downtime update process.

Recommended Approach

For this design choice, I recommend Dynamic HMAC for the following reasons. It was crafted from a deep dive into this article, including research and assessments of common webhook security practices.

  1. HTTPS is a given.
  2. IP Whitelisting is problematic because IP ranges often change. This would require much maintenance and monitoring, and webhook failures could occur without notice.
  3. Header secrets are unnecessary if HMAC is used.
  4. Dynamic HMAC (dHMAC) is a superset of header secrets and DHSR without any of the drawbacks of DHSR.

How Dynamic HMAC works

dHMAC utilizes an encryption technique that is simple in design but very difficult to breach. It uses an algorithm that combines various data values from the Airtable record, the current environment, such as the date and time, and/or a random GUID. This algorithm computes a unique “fingerprint” of the originating webhook transaction.

In a nutshell, the algorithm assumes the fingerprint has certain attributes that are used in this workflow:

  1. The webhook trigger script (on the Airtable side) computes a SHA256-bit encryption key based on data specific to that webhook call.
  2. The webhook listener (on the Make side) uses the same data in that webhook call to compute a SHA256-bit encryption key INDEPENDENTLY of the webhook caller.
  3. The webhook listener rejects any webhook calls where the two encrypted keys are not identical. I refer to this as the acceptance algorithm.

Functionally, the fingerprint is included in the data payload sent to the webhook listener, which is then required to apply the same algorithm to compute independently the fingerprint of the data received. It then compares the originating fingerprint with the received fingerprint, and if they match, the webhook is accepted as authentic. All others are rejected.

As noted earlier, this approach is a superset of the header-bound secret approach because every fingerprint is encrypted and changes with every webhook call. Even back-to-back webhook calls for the same Airtable record would have unique encrypted values - a unique fingerprint.

This approach also ensures that even if a hacker could sniff out (HTTPS) data payloads, they would need to understand the nature of the algorithm that generates the fingerprint. Since that value is a combination of data in the webhook payload transformed into an encrypted key, and at least one of the data values is partial, nefarious actors could never replicate a valid payload and push data to the webhook endpoint that could be mistaken as authentic. As such, any webhook calls intercepted and modified en route would be rejected.

Potential Risks

Every security approach has flaws. Risks exist in the calling platform, the security algorithm, and the webhook listener platform. If we can mitigate the risks in the algorithm and control who has access to the webhook calling and webhook receiving platforms, we can be confident that breaches are significantly less likely to occur.

Access to Webhook Caller and Webhook Listener Platform

A single person with access to the webhook origin calling platform (Airtable) and the webhook listener platform (Make) poses the greatest risk of an inside job or an unintended opportunity for a breach. This is no different from an auditor insisting that bookkeepers not have signature access to bank funds.

Modifying the Webhook Caller Script

A possible risk is that someone uses the same Airtable script to send webhooks from a different base, thus contaminating the data. This would be an inside job, but inherently, it would be deflected. An insider could change the script and use it with a different base. Still, when the Make scenario reads the record passed from the new script, it will fail because it has only been given authority to reference record IDs in a specific base and table.

Removing the Make Security Test

The Make scenario requires inserting a filter that computes the received fingerprint and compares it with the originating fingerprint. The weakness is that a Make user can remove the filter from the flow that governs the application of the acceptance algorithm. Make’s own security controls cannot mitigate this risk because a legitimate Make user could remove this filter, and all webhook events would be processed.

Dynamic HMAC Algorithm

Airtable Webhook Security Functions Library

Computing and utilizing SHA256-bit encrypted keys requires several helper functions. This library is typically placed at the end of Airtable script blocks or automation scripts that call Make webhooks. The complete source is provided below.

Airtable SHA256-bit Encryption Algorithm

The javascript algorithm that determines the fingerprint key for any given webhook event comprises several variables and a JSON payload sent to the Make webhook listener.

{
  "recordID" : "reclk2Q5bLVUeePBs",
  "dateTime" : "1694877502734",
  "trxID"    : "b85554ac60f41a4b5e490d9765ef9e61a830e5c9979665fd52d4230cf827a309",
  "trxGuid"  : "7a6789e5-c994-4138-9179-a74fd4d230e3",
  "trxCode"  : 21
}

Example Webhook Payload with SHA256-bit Encryption Fingerprint

The payload elements are intentionally labeled to obfuscate understanding of what these values represent. The trxID field is the SHA256-bit encryption key. The trxGuid is exactly what it indicates - a random v4 36-byte GUID. The trxCode field contains a tertiary value that makes it almost impossible to guess how many characters of the trxGuid are used to compute the SHA256-bit encryption key.

Passing these fields in addition to the dateTime and recordID over to the Make webhook listener, we set the stage for a signed fingerprint test that can determine if the webhook call is authentic.

trxGuid

This value is unique for every webhook event.

// generate the guid
var trxGuid = uuidv4();

trxCode

This value is random for every webhook event expressed as integer between 1 and 36.

// generate the trx code
var trxCode = getRandomInt(36);
trxCode = (trxCode > 0) ? trxCode : 1;

trxID

This value is the SHA256-bit encrypted key also known as the webhook caller fingerprint. The only static value in the fingerprint is the Airtable record ID. The rest are dynamic; combined, these are unique for every webhook event.

// generate the trx id (sha256 fingerprint)
var trxID = sha256(recordID + "::" + dateTime.toString() + "::" + trxGuid.substring(0, trxCode));

Make SHA256-bit Encryption Algorithm

When the Make webhook receives the payload, it must use the data values in the JSON payload to compute its own version of the fingerprint. The filter between the webhook component and the Airtable update serves as the security test. This can be applied to any scenario and in multiple workflow contexts.


Make Filter: SHA256-bit Security Test

Example Airtable Webhook Caller

This script demonstrates how simple it is to call a webhook with all the data needed to secure the integration with SHA256-bit encryption as a verifiable transaction fingerprint.

/*
   ***********************************************************
   Super Secure Make Webhooks
   Copyright (c) 2023 by Global Technologies Corporation
   ALL RIGHTS RESERVED
   ***********************************************************
*/

output.markdown('# Super Secure Make Webhook!');

// get the record id from an automation or button press
// let inputConfig = input.config();
// let recordID = inputConfig.recordID;

let recordID = "recdWf8rGreiXBC1u";
console.log(recordID);

// generate the date/time value
var dateTime = new Date().getTime();
console.log(dateTime);

// generate the trx guid
var trxGuid = uuidv4();
console.log(trxGuid);
console.log(trxGuid.length);

// generate the trx code
var trxCode = getRandomInt(36);
trxCode = (trxCode > 0) ? trxCode : 1;
console.log(trxCode);

// generate the sha256 hash
var trxID = sha256(recordID + "::" + dateTime.toString() + "::" + trxGuid.substring(0, trxCode));
console.log(trxID);

var body = {
    "recordID"   : recordID,
    "dateTime"   : dateTime.toString(),
    "trxID"      : trxID,
    "trxGuid"    : trxGuid,
    "trxCode"    : trxCode
}

let url = "https://hook.eu2.make.com/lubr5vyi6th9wiwja029x7nsfrs2qma9";
console.log(url);

let options   = {
    "method": "POST",
    "headers": {
        "Content-Type"  : "application/json",
    },
    "body" : JSON.stringify(body)
}
console.log(await fetch(url, options));

Airtable Webhook Security Functions Library

This collection of javascript functions is required for any webhook caller scripts in Airtable. They are added to the bottom of Airtable script block or automation script.

/*
   ***********************************************************
   Airtable Webhook Security Functions Library
   Copyright (c) 2023 by Global Technologies Corporation
   ALL RIGHTS RESERVED
   ***********************************************************
*/

//
// generate random guid
//
function uuidv4() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    .replace(/[xy]/g, function (c) {
        const r = Math.random() * 16 | 0, 
            v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

//
// get random int
//
function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

//
// generate sha256 encryption token 
//
function sha256(ascii) {
    function rightRotate(value, amount) {
        return (value>>>amount) | (value<<(32 - amount));
    };
    
    var mathPow = Math.pow;
    var maxWord = mathPow(2, 32);
    var lengthProperty = 'length'
    var i, j; // Used as a counter across the whole file
    var result = ''

    var words = [];
    var asciiBitLength = ascii[lengthProperty]*8;
    
    //* caching results is optional - remove/add slash from front of this line to toggle
    // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
    // (we actually calculate the first 64, but extra values are just ignored)
    var hash = sha256.h = sha256.h || [];
    // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
    var k = sha256.k = sha256.k || [];
    var primeCounter = k[lengthProperty];
    /*/
    var hash = [], k = [];
    var primeCounter = 0;
    //*/

    var isComposite = {};
    for (var candidate = 2; primeCounter < 64; candidate++) {
        if (!isComposite[candidate]) {
            for (i = 0; i < 313; i += candidate) {
                isComposite[i] = candidate;
            }
            hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
            k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
        }
    }
    
    ascii += '\x80' // Append Ƈ' bit (plus zero padding)
    while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding
    for (i = 0; i < ascii[lengthProperty]; i++) {
        j = ascii.charCodeAt(i);
        if (j>>8) return; // ASCII check: only accept characters in range 0-255
        words[i>>2] |= j << ((3 - i)%4)*8;
    }
    words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
    words[words[lengthProperty]] = (asciiBitLength)
    
    // process each chunk
    for (j = 0; j < words[lengthProperty];) {
        var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
        var oldHash = hash;
        // This is now the undefinedworking hash", often labelled as variables a...g
        // (we have to truncate as well, otherwise extra entries at the end accumulate
        hash = hash.slice(0, 8);
        
        for (i = 0; i < 64; i++) {
            var i2 = i + j;
            // Expand the message into 64 words
            // Used below if 
            var w15 = w[i - 15], w2 = w[i - 2];

            // Iterate
            var a = hash[0], e = hash[4];
            var temp1 = hash[7]
                + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
                + ((e&hash[5])^((~e)&hash[6])) // ch
                + k[i]
                // Expand the message schedule if needed
                + (w[i] = (i < 16) ? w[i] : (
                        w[i - 16]
                        + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0
                        + w[i - 7]
                        + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1
                    )|0
                );
            // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
            var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
                + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj
            
            hash = [(temp1 + temp2)|0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
            hash[4] = (hash[4] + temp1)|0;
        }
        
        for (i = 0; i < 8; i++) {
            hash[i] = (hash[i] + oldHash[i])|0;
        }
    }
    
    for (i = 0; i < 8; i++) {
        for (j = 3; j + 1; j--) {
            var b = (hash[i]>>(j*8))&255;
            result += ((b < 16) ? 0 : '') + b.toString(16);
        }
    }
    return result;
};
4 Likes