From 037ab5f3dea0e39798836ff3194d4cc91e1d7652 Mon Sep 17 00:00:00 2001 From: franks883 Date: Tue, 21 Oct 2025 18:53:03 +0100 Subject: [PATCH 1/3] Create README.md --- .../README.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md diff --git a/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md new file mode 100644 index 0000000000..6cc7fc80a1 --- /dev/null +++ b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/README.md @@ -0,0 +1,26 @@ +# Webhook receiver with HMAC SHA-256 validation + +## What this solves +Inbound webhooks should be verified to ensure the payload really came from the sender. This receiver validates an `X-Signature` header containing an HMAC SHA-256 of the request body using a shared secret. Invalid signatures return HTTP 401. + +## Where to use +- Scripted REST API resource script +- Include the `HmacUtils` Script Include in the same app or global + +## How it works +- Reads raw request body and the `X-Signature` header +- Computes HMAC SHA-256 using the shared secret +- Compares in constant time to avoid timing attacks +- If valid, inserts the payload into a target table or queues it for processing + +## Configure +- Set `SHARED_SECRET` (prefer credentials or encrypted properties) +- Update `TARGET_TABLE` for successful inserts + +## References +- Scripted REST APIs + https://www.servicenow.com/docs/bundle/zurich-application-development/page/build/applications/task/create-scripted-rest-api.html +- REST API request/response objects + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideHTTPRequest/concept/c_scripted-rest-api-request.html +- Java crypto (used server-side) + https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/Script/server_apis/concept/java-use.html From 88864d2a31fa4834ea8213eacc4ac2c93332e676 Mon Sep 17 00:00:00 2001 From: franks883 Date: Tue, 21 Oct 2025 18:53:36 +0100 Subject: [PATCH 2/3] Create HmacUtils.js --- .../HmacUtils.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js diff --git a/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js new file mode 100644 index 0000000000..88e29d03cb --- /dev/null +++ b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/HmacUtils.js @@ -0,0 +1,35 @@ +// Script Include: HmacUtils +// Purpose: Compute HMAC SHA-256 and constant-time compare. + +var HmacUtils = Class.create(); +HmacUtils.prototype = { + initialize: function() {}, + + hmacSha256Hex: function(secret, message) { + var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256'); + var key = new Packages.javax.crypto.spec.SecretKeySpec( + new Packages.java.lang.String(secret).getBytes('UTF-8'), + 'HmacSHA256' + ); + mac.init(key); + var raw = mac.doFinal(new Packages.java.lang.String(message).getBytes('UTF-8')); + + var sb = new Packages.java.lang.StringBuilder(); + for (var i = 0; i < raw.length; i++) { + var hex = Packages.java.lang.Integer.toHexString((raw[i] & 0xff) | 0x100).substring(1); + sb.append(hex); + } + return sb.toString(); + }, + + constantTimeEquals: function(a, b) { + var A = String(a || ''); + var B = String(b || ''); + if (A.length !== B.length) return false; + var diff = 0; + for (var i = 0; i < A.length; i++) diff |= A.charCodeAt(i) ^ B.charCodeAt(i); + return diff === 0; + }, + + type: 'HmacUtils' +}; From 376ae07f1afbbbbf1fd0a25cdf12b2b7a42ac780 Mon Sep 17 00:00:00 2001 From: franks883 Date: Tue, 21 Oct 2025 18:53:54 +0100 Subject: [PATCH 3/3] Create WebhookHmacReceiver.js --- .../WebhookHmacReceiver.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js diff --git a/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js new file mode 100644 index 0000000000..9972c552a5 --- /dev/null +++ b/Integration/Scripted REST Api/Webhook receiver with HMAC SHA-256 validation/WebhookHmacReceiver.js @@ -0,0 +1,43 @@ +// Scripted REST API Resource Script: Webhook receiver with HMAC validation +(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) { + var SHARED_SECRET = gs.getProperty('x_acme.webhook.secret', ''); + var TARGET_TABLE = 'x_acme_inbound_webhook'; // replace with your table + + try { + var body = request.body && request.body.data ? request.body.data : ''; + var signature = request.getHeader('X-Signature') || ''; // hex HMAC hash + + if (!SHARED_SECRET) { + response.setStatus(500); + response.setBody({ error: 'Server not configured' }); + return; + } + if (!signature || !body) { + response.setStatus(400); + response.setBody({ error: 'Missing signature or body' }); + return; + } + + var util = new HmacUtils(); + var expected = util.hmacSha256Hex(SHARED_SECRET, body); + + if (!util.constantTimeEquals(expected, signature)) { + response.setStatus(401); + response.setBody({ error: 'Invalid signature' }); + return; + } + + // Valid payload: insert a record for processing + var rec = new GlideRecord(TARGET_TABLE); + rec.initialize(); + rec.payload = body; + rec.signature = signature; + rec.insert(); + + response.setStatus(200); + response.setBody({ ok: true }); + } catch (e) { + response.setStatus(500); + response.setBody({ error: String(e) }); + } +})(request, response);