Skip to content

Commit 8749825

Browse files
authored
Merge branch 'ServiceNowDevProgram:main' into cancel-running-flow-executions
2 parents 959b848 + 45dfc4b commit 8749825

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Script Include: HmacUtils
2+
// Purpose: Compute HMAC SHA-256 and constant-time compare.
3+
4+
var HmacUtils = Class.create();
5+
HmacUtils.prototype = {
6+
initialize: function() {},
7+
8+
hmacSha256Hex: function(secret, message) {
9+
var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256');
10+
var key = new Packages.javax.crypto.spec.SecretKeySpec(
11+
new Packages.java.lang.String(secret).getBytes('UTF-8'),
12+
'HmacSHA256'
13+
);
14+
mac.init(key);
15+
var raw = mac.doFinal(new Packages.java.lang.String(message).getBytes('UTF-8'));
16+
17+
var sb = new Packages.java.lang.StringBuilder();
18+
for (var i = 0; i < raw.length; i++) {
19+
var hex = Packages.java.lang.Integer.toHexString((raw[i] & 0xff) | 0x100).substring(1);
20+
sb.append(hex);
21+
}
22+
return sb.toString();
23+
},
24+
25+
constantTimeEquals: function(a, b) {
26+
var A = String(a || '');
27+
var B = String(b || '');
28+
if (A.length !== B.length) return false;
29+
var diff = 0;
30+
for (var i = 0; i < A.length; i++) diff |= A.charCodeAt(i) ^ B.charCodeAt(i);
31+
return diff === 0;
32+
},
33+
34+
type: 'HmacUtils'
35+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Webhook receiver with HMAC SHA-256 validation
2+
3+
## What this solves
4+
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.
5+
6+
## Where to use
7+
- Scripted REST API resource script
8+
- Include the `HmacUtils` Script Include in the same app or global
9+
10+
## How it works
11+
- Reads raw request body and the `X-Signature` header
12+
- Computes HMAC SHA-256 using the shared secret
13+
- Compares in constant time to avoid timing attacks
14+
- If valid, inserts the payload into a target table or queues it for processing
15+
16+
## Configure
17+
- Set `SHARED_SECRET` (prefer credentials or encrypted properties)
18+
- Update `TARGET_TABLE` for successful inserts
19+
20+
## References
21+
- Scripted REST APIs
22+
https://www.servicenow.com/docs/bundle/zurich-application-development/page/build/applications/task/create-scripted-rest-api.html
23+
- REST API request/response objects
24+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideHTTPRequest/concept/c_scripted-rest-api-request.html
25+
- Java crypto (used server-side)
26+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/Script/server_apis/concept/java-use.html
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Scripted REST API Resource Script: Webhook receiver with HMAC validation
2+
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
3+
var SHARED_SECRET = gs.getProperty('x_acme.webhook.secret', '');
4+
var TARGET_TABLE = 'x_acme_inbound_webhook'; // replace with your table
5+
6+
try {
7+
var body = request.body && request.body.data ? request.body.data : '';
8+
var signature = request.getHeader('X-Signature') || ''; // hex HMAC hash
9+
10+
if (!SHARED_SECRET) {
11+
response.setStatus(500);
12+
response.setBody({ error: 'Server not configured' });
13+
return;
14+
}
15+
if (!signature || !body) {
16+
response.setStatus(400);
17+
response.setBody({ error: 'Missing signature or body' });
18+
return;
19+
}
20+
21+
var util = new HmacUtils();
22+
var expected = util.hmacSha256Hex(SHARED_SECRET, body);
23+
24+
if (!util.constantTimeEquals(expected, signature)) {
25+
response.setStatus(401);
26+
response.setBody({ error: 'Invalid signature' });
27+
return;
28+
}
29+
30+
// Valid payload: insert a record for processing
31+
var rec = new GlideRecord(TARGET_TABLE);
32+
rec.initialize();
33+
rec.payload = body;
34+
rec.signature = signature;
35+
rec.insert();
36+
37+
response.setStatus(200);
38+
response.setBody({ ok: true });
39+
} catch (e) {
40+
response.setStatus(500);
41+
response.setBody({ error: String(e) });
42+
}
43+
})(request, response);

0 commit comments

Comments
 (0)