helpscout / helpscout-api-php

PHP Wrapper for the Help Scout API
MIT License
98 stars 62 forks source link

Incoming Webhooks Signature Validation #292

Closed webdj-agency closed 2 years ago

webdj-agency commented 2 years ago

Hello there! We are actively using the Help Scout ticket system in KryptoniteWP and have developed "FluentCRM - Help Scout Integration" Wordpress plugin which works with Help Scout Webhooks.

Current behavior

Our problem is that the $_SERVER['HTTP_X_HELPSCOUT_SIGNATURE'] header sent by Help Scout Webhooks is not equal to the value returned by this check: https://developer.helpscout.com/webhooks/#verifying

The same code is found in generateSignature() method from https://github.com/helpscout/helpscout-api-php/blob/master/src/Webhooks/IncomingWebhook.php#L86 which uses PHP code:

base64_encode(
    hash_hmac(
        'sha1',
        $body,
        $this->secret,
        true
    )
);

There is an endpoint in our Help Scout Webhooks settings:

Bildschirmfoto 2022-05-25 um 12 58 03

Every time the Help Scout ticket has been opened by our customer, your Webhooks app sends a request to this endpoint on our Wordpress dev site. The request body we are getting from Help Scout looks like:

  "ticket": {
    "id": "1234567890",
    "number": "12345",
    "subject": "Test FluentCRM HelpScout"
  },
  "customer": {
    "id": "12345678",
    "fname": "<CUSTOMER_NAME>",
    "lname": "<CUSTOMER_LAST_NAME>",
    "email": "<CUSTOMER_EMAIL>",
    "emails": [
      "<CUSTOMER_EMAIL>"
    ]
  },
  "user": {
    "fname": "<USER_NAME>",
    "lname": "<USER_LAST_NAME>",
    "id": 123456,
    "role": "owner",
    "convRedirect": 0
  },
  "mailbox": {
    "id": 12345,
    "email": "<OUR_SUPPORT_EMAIL>"
  }
}

Our Wordpress plugin takes the customer email from request body, looks for this customer in FluentCRM contacts ans sends back a response with some HTML (JSON encoded).

We have a ticket for test: https://secure.helpscout.net/conversation/1896754970/47420?folderId=407329 where the response HTML outputs such widget: Screenshot from 2022-05-25 12-38-39

So if we ignore the signature validation: everything works fine.

According to the Webhooks instructions: Signature Validation Help Scout will generate a signature using the secret key you provided. The Help Scout-generated signature will be available in the headers as X-HELPSCOUT-SIGNATURE. You can calculate the same signature on your side, using the same secret key. If the signatures match, you know the request is from Help Scout and can proceed with providing data. If the signatures do not match, we recommend discarding the request. It works exactly like how we handle webhooks (see the Verifying section).

There is no request header X-HELPSCOUT-SIGNATURE as it is said in the docs but we have HTTP_X_HELPSCOUT_SIGNATURE. So we consider this one.

In the test ticket mentioned above we use the DEBUG MODE which gives us Request Headers:

POST /wp-json/fluentcrm-helpscout-api/v2/contact HTTP/1.1
Host: <OUR_HOST>
User-Agent: Help Scout (http://www.helpscout.com/) help@helpscout.com
Accept: */*
Connection: close
Content-Type: application/json; charset=UTF-8
X-HelpScout-Signature: 9BbKrpunYODjoD0bimBgfPULqbA=
tracestate: 65510@nr=0-0-65510-30056445-5031073d45149c97-084634a258266bd5-1-1.914740-1653651077572
newrelic: eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiNjU1MTAiLCJhcCI6IjMwMDU2NDQ1IiwiaWQiOiI1MDMxMDczZDQ1MTQ5Yzk3IiwidHIiOiJhMmY3Y2E5YjJiODUxYzgyM2IwNGFhY2EyY2U5NmEyNCIsInR4IjoiMDg0NjM0YTI1ODI2NmJkNSIsInByIjoxLjkxNDc0LCJzYSI6dHJ1ZSwidGkiOjE2NTM2NTEwNzc1NzJ9fQ==
traceparent: 00-a2f7ca9b2b851c823b04aaca2ce96a24-5031073d45149c97-01
Content-Length: 348

as you can see there is X-HelpScout-Signature header which is 9BbKrpunYODjoD0bimBgfPULqbA=

This simple check using the Request Body (we replaced the sensitive data here):

$body = '{"ticket":{"id":"1234567890","number":"12345","subject":"Test FluentCRM HelpScout"},"customer":{"id":"12345678","fname":"<CUSTOMER_NAME>","lname":"<CUSTOMER_LAST_NAME>","email":"<CUSTOMER_EMAIL>","emails":["<CUSTOMER_EMAIL>"]},"user":{"fname":"<USER_NAME>","lname":"<USER_LAST_NAME>","id":123456,"role":"user","convRedirect":0},"mailbox":{"id":12345,"email":"<OUR_SUPPORT_EMAIL>"}}';

$secret_key = '<OUR_SECRET_KEY>';

$hash = base64_encode( hash_hmac( 'sha1', $body, $secret_key, true ) );

gives us the value K5Mku1sULDK9q6lDy1Ol3iAxgcI= which doesn't match the 9BbKrpunYODjoD0bimBgfPULqbA=

Maybe we should add something to the $body variable? Which is the right way to validate the signature?

flowdee commented 2 years ago

Would be great to get a solution for this asap @miguelrs 😇

miguelrs commented 2 years ago

Hey @flowdee. Thanks for opening this issue!

I've been making tests and the verification of the x-helpscout-signature header works fine for me, using the code provided in the docs you linked.

I'm wondering if, when doing your tests, have you considered this bit 👇 about non-ASCII characters mentioned in the docs?

Signatures are calculated based on the raw request body passed to your servers by Help Scout. This means that if non-ASCII characters are contained in the payload, you will need to calculate the signature based on the escaped, transliterated string passed to you by Help Scout.

You can see this clearly in this test I made using webhook.site. I added a note with non-ASCII characters to a conversation and waited for the webhook to arrive.

In the first picture, you can see that, if we check [] Format JSON then we can see the preview properly, but if you try to validate that exact string with those characters, the verification will fail 🚫

CleanShot 2022-05-31 at 18 47 51@2x

In the second picture, you can see that unchecking [] Format JSON will give you the raw data, which is exactly what we give to you and what we use to generate the signature. If you use that escaped string, then the verification passes ✅

CleanShot 2022-05-31 at 18 50 53@2x

webdj-agency commented 2 years ago

@miguelrs thanks for help. the issue was at our side. wp_generate_password function adds non-ASCII by default. fixed