HubSpot form events have a security vulnerability if you use their boilerplate JS

Update 2022-01-16: Discovered Calendly made much the same mistake and blogged about that here.

Worked on a li’l HubSpot JS project this week and noticed a pretty glaring vulnerability in their boilerplate “global form events” code.

The vulnerability could lead to a range of hacks — from the minor to the all-hands-how-did-this-happen — depending on your particular implementation. But you don’t want even the minor end, trust me!

Here’s HubSpot’s suggested code to run additional actions when a form has been posted:[1]

window.addEventListener('message', event => {
   if(event.data.type === 'hsFormCallback' && event.data.eventName === 'onFormSubmit') {
       someAnalyticsLib('formSubmitted');
       console.log("Form Submitted!");
   }
});

(That this code doesn’t work on any version of IE, thanks to unnecessary use of the => operator, is aggravating to begin with.)

That call to someAnalyticsLib is meant to symbolize sending an event to GA or Clicky or similar. But you could be doing anything you want in response to the form being submitted.

All the field values are present in the event object (event.data.data is a JS array of objects, one for each form field). So you could use the form values, including hidden fields, to determine the Thank You URL. You might post the values into a more detailed form to continue signup within your app. Or you might pass them off-site, to a remote URL that’s itself stored in a hidden field.

Whatever happens with the data must be controlled by your code + the end user’s choices, right? No 3rd-party — someone other than you and your leads — should be able to send data anywhere.

So let’s check out how the incoming message is validated.

HubSpot’s (notably incomplete) event validation

The code checks that the nested data.type property (an otherwise arbitrary string) of the incoming MessageEvent is set to the known HubSpot value "hsFormCallback". And it checks that the data.eventName is set to the known value "onFormSubmit". (A few other HubSpot-specific eventNames are also documented.)

But it does not check that the HubSpot forms code sent the message. In fact, anybody running any site can send such a matching message to your page, because the code fails to validate the origin of the MessageEvent.

All an attacker has to do is get the end user to navigate to their site. Then they can IFRAME your page with the HubSpot form and send that page a correctly-formed message, making it execute your code.

They can send such a message to the IFRAME even though the outer page can’t directly read the contents of the IFRAME due to cross-domain restrictions.

Check out this simple HTML:

<iframe 
  name="hubspotTarget" 
  src="https://yourvulnerablesite.example.com/lp.html"
  style="display:none;"
  onload="hubspotTarget.postMessage({ type: 'hsFormCallback', eventName: 'onFormSubmit', data : [{ name : 'email', value : 'gotcha@hacker.test' }] },'https://yourvulnerablesite.example.com');"
></iframe>

If your  lp.html has code based on the boilerplate:

if (event.data.type === "hsFormCallback" &&
    event.data.eventName === "onFormSubmit") {
      // do something mildly-to-extremely dangerous
}

Then it will immediately execute that dangerous code, exactly as if the form had been filled out by an end user with the attacker-provided email value.

Note (this is really important when picturing the security impact) the end user has no idea this happened! The IFRAME is hidden.

So the attacker is able to make an army of end users unwitting accomplices in leaking data, attempting to penetrate an authenticated app, and so on. You should shudder at the havoc caused in the worst case.

Such an attack bypasses XSRF protection

It’s typical to include a Cross-Site Request Forgery (XSRF) token with authenticated and/or sensitive sessions.

In brief, the idea is that while a page from another site can throw data at your server — including previously set cookies — it can’t read the unique value your server requires with every request in that session. Forgery can thus be detected on the server side and the request ignored.

(The request itself — the “XSR” in “XSRF” — can’t be stopped completely from being sent, as HTML always allows cross-domain <form action> values, for one thing.).

But the attack here wouldn’t be detected, since the request is being made by your own code, which already includes the XSRF token.

Solving it

Yeah, I always rant for a bit instead of cutting to the solution. But otherwise, why blog?

Here’s how you make sure no outsider can fake their way into your event listener, and support IE 11 as well:

window.addEventListener("message", function(event) {
  if (event.data.type === "hsFormCallback" &&
    event.data.eventName === "onFormSubmit" &&
    event.origin === document.location.origin) {
    // do something
  }
});

By checking if the origin of the MessageEvent is the same as the origin of your page, you ensure that malicious parent windows — or child windows, for that matter can’t invoke your code. Malicious messages can still be sent, but your code will ignore them because the origins don’t match.

Stay secure!

Notes

[1] HubSpot’s onFormSubmit is the rough, but not exact, equivalent of onSuccess in Marketo.