Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import crypto from "crypto";

export default {
name: "Verify HMAC Signature",
version: "0.0.1",
key: "http-verify-hmac-signature",
description: "Validate HMAC signature for incoming HTTP webhook requests. Make sure to configure the HTTP trigger to \"Return a custom response from your workflow\".",
type: "action",
props: {
http: "$.interface.http",
secret: {
type: "string",
label: "Secret",
description: "Your secret key used for validation",
secret: true,
},
signature: {
type: "string",
label: "Signature",
description: "The HMAC signature received from the incoming webhook, typically provided in a specific HTTP header",
},
bodyRaw: {
type: "string",
label: "Raw Body",
description: "The raw request body received from the webhook caller, provided as a string without any parsing or modifications",
},
Comment on lines +17 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for signature and bodyRaw props

For security-critical components, it's important to validate inputs before processing. Consider adding:

  • Minimum length requirement for the signature
  • Maximum length limits for both signature and bodyRaw to prevent DoS
  • Format validation for the signature (hex string)
   signature: {
     type: "string",
     label: "Signature",
     description: "The HMAC signature received from the incoming webhook, typically provided in a specific HTTP header",
+    validate: (value) => {
+      if (!/^[0-9a-f]{64}$/i.test(value)) {
+        throw new Error("Signature must be a 64-character hexadecimal string");
+      }
+    },
   },
   bodyRaw: {
     type: "string",
     label: "Raw Body",
     description: "The raw request body received from the webhook caller, provided as a string without any parsing or modifications",
+    validate: (value) => {
+      if (value.length > 1048576) { // 1MB limit
+        throw new Error("Request body exceeds maximum size of 1MB");
+      }
+    },
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
signature: {
type: "string",
label: "Signature",
description: "The HMAC signature received from the incoming webhook, typically provided in a specific HTTP header",
},
bodyRaw: {
type: "string",
label: "Raw Body",
description: "The raw request body received from the webhook caller, provided as a string without any parsing or modifications",
},
signature: {
type: "string",
label: "Signature",
description: "The HMAC signature received from the incoming webhook, typically provided in a specific HTTP header",
validate: (value) => {
if (!/^[0-9a-f]{64}$/i.test(value)) {
throw new Error("Signature must be a 64-character hexadecimal string");
}
},
},
bodyRaw: {
type: "string",
label: "Raw Body",
description: "The raw request body received from the webhook caller, provided as a string without any parsing or modifications",
validate: (value) => {
if (value.length > 1048576) { // 1MB limit
throw new Error("Request body exceeds maximum size of 1MB");
}
},
},

customResponse: {
type: "boolean",
label: "Return Error to Webhook Caller",
description: "If `True`, returns a `401: Invalid credentials` error in the case of invalid authorization. **Make sure to configure the HTTP trigger to \"Return a custom response from your workflow\"**. If `False`, does not return a custom response in the case of invalid auth.",
default: true,
},
},
methods: {
_checkHmac(secret, signature, bodyRaw) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(bodyRaw, "utf8")
.digest();

const signatureBuffer = Buffer.from(signature, "hex");
if (signatureBuffer.length !== expectedSignature.length) {
return false;
}
return crypto.timingSafeEqual(signatureBuffer, expectedSignature);
},
Comment on lines +35 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance input validation and add JSDoc documentation

While the core HMAC validation is secure, there are some improvements needed:

  1. Add explicit null/empty input validation
  2. Document the expected secret key format and encoding
  3. Add JSDoc for better maintainability
+  /**
+   * Validates an HMAC signature against a request body
+   * @param {string} secret - The secret key (UTF-8 encoded)
+   * @param {string} signature - Hexadecimal signature string
+   * @param {string} bodyRaw - Raw request body
+   * @returns {boolean} True if signature is valid
+   * @throws {Error} If inputs are null/empty or improperly formatted
+   */
   _checkHmac(secret, signature, bodyRaw) {
+    if (!secret || !signature || !bodyRaw) {
+      throw new Error("Missing required parameters");
+    }
+
     const expectedSignature = crypto
       .createHmac("sha256", secret)
       .update(bodyRaw, "utf8")
       .digest();

     const signatureBuffer = Buffer.from(signature, "hex");
     if (signatureBuffer.length !== expectedSignature.length) {
       return false;
     }
     return crypto.timingSafeEqual(signatureBuffer, expectedSignature);
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_checkHmac(secret, signature, bodyRaw) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(bodyRaw, "utf8")
.digest();
const signatureBuffer = Buffer.from(signature, "hex");
if (signatureBuffer.length !== expectedSignature.length) {
return false;
}
return crypto.timingSafeEqual(signatureBuffer, expectedSignature);
},
/**
* Validates an HMAC signature against a request body
* @param {string} secret - The secret key (UTF-8 encoded)
* @param {string} signature - Hexadecimal signature string
* @param {string} bodyRaw - Raw request body
* @returns {boolean} True if signature is valid
* @throws {Error} If inputs are null/empty or improperly formatted
*/
_checkHmac(secret, signature, bodyRaw) {
if (!secret || !signature || !bodyRaw) {
throw new Error("Missing required parameters");
}
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(bodyRaw, "utf8")
.digest();
const signatureBuffer = Buffer.from(signature, "hex");
if (signatureBuffer.length !== expectedSignature.length) {
return false;
}
return crypto.timingSafeEqual(signatureBuffer, expectedSignature);
},

},
async run({ $ }) {
const valid = this._checkHmac(
this.secret,
this.signature,
this.bodyRaw,
);

if (!valid) {
if (this.customResponse) {
await $.respond({
status: 401,
headers: {},
body: "Invalid credentials",
});
}
return $.flow.exit("Invalid credentials");
}
Comment on lines +55 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add security logging and secure headers

For security monitoring and hardening:

  1. Add logging for validation failures
  2. Include security headers in the response
  3. Consider rate limiting for failed attempts
     if (!valid) {
+      console.error("HMAC validation failed", {
+        timestamp: new Date().toISOString(),
+        ip: $.event.client_ip,
+      });
+
       if (this.customResponse) {
         await $.respond({
           status: 401,
-          headers: {},
+          headers: {
+            "Content-Type": "text/plain",
+            "X-Content-Type-Options": "nosniff",
+            "Cache-Control": "no-store",
+          },
           body: "Invalid credentials",
         });
       }
       return $.flow.exit("Invalid credentials");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!valid) {
if (this.customResponse) {
await $.respond({
status: 401,
headers: {},
body: "Invalid credentials",
});
}
return $.flow.exit("Invalid credentials");
}
if (!valid) {
console.error("HMAC validation failed", {
timestamp: new Date().toISOString(),
ip: $.event.client_ip,
});
if (this.customResponse) {
await $.respond({
status: 401,
headers: {
"Content-Type": "text/plain",
"X-Content-Type-Options": "nosniff",
"Cache-Control": "no-store",
},
body: "Invalid credentials",
});
}
return $.flow.exit("Invalid credentials");
}


$.export("$summary", "HTTP request successfully authenticated");
},
};
2 changes: 1 addition & 1 deletion components/http/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/http",
"version": "0.4.1",
"version": "0.5.0",
"description": "Pipedream Http Components",
"main": "http.app.js",
"keywords": [
Expand Down
Loading