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

Update 2022-04-11: Calendly modified their JS to properly check the origin. Nice!

Upon looking at their latest API docs, looks like Calendly (which I love dearly) committed the same sin as HubSpot (which I don’t). Sigh.

As I mentioned in my post on the HubSpot vulnerability, you can’t process PostMessage messages without validating the origin property. Anybody can send a message to a window, but they can’t fake the message’s origin.

The Calendly docs suggest this simple code to capture whenever somebody  successfully books a meeting:[1]

function isCalendlyEvent(e) {
  return e.data.event && e.data.event.indexOf("calendly") === 0;
};
 
window.addEventListener( "message", function(e) {
    if (isCalendlyEvent(e)) {
      console.log(e.data);
    }
});
Calendly’s suggested code

First, gotta question why isCalendlyEvent is a global function (this has nothing to do with the vulnerability, though!) so let me first wrap everything in an IIFE to be more conservative:

(function(){
   
   function isCalendlyEvent(e) {
      return e.data.event && e.data.event.indexOf("calendly") === 0;
   };

   window.addEventListener( "message", function(e) {
      if (isCalendlyEvent(e)) {
         console.log(e.data);
      }
   });

})();
Calendly’s code kept out of top-level scope, but otherwise unchanged

Now let’s see what it does:

(1) Checks whether the inbound message data has a property event (that’s actually a custom property name, somewhat confusingly named).

(2) If it has e.data.event, checks if the value e.data.event starts with the string calendly. (All the Calendly-generated events start with calendly., like calendly.date_and_time_selected, calendly.event_scheduled, etc. Not sure why the pattern leaves out the trailing dot.)

(3) If (2) matches, assumes you’re good to do whatever you want with the data, such as adding it to a hidden form field and auto-submitting the form.

That’s it. At no point does it validate that the message was actually sent by the Calendly widget (IFRAME) because it doesn’t check the e.origin.

Therefore, the vulnerability is the same as the HubSpot vulnerability. I’d ask that you read that section of the earlier post rather than repeating myself.

Fixing it

Yes, doing it right makes the example code a little longer and less “simple,” but that’s the price of security:

(function(){
   
   function isCalendlyEvent(e) {
      return e.origin === "https://calendly.com" && e.data.event && e.data.event.indexOf("calendly.") === 0;
   };

   window.addEventListener( "message", function(e) {
      if (isCalendlyEvent(e)) {
         console.log("Calendly event type", e.data.event);
         
         if( Object.keys(e.data.payload).length ) {
            console.log("Calendly event details", e.data.payload);
         } else {
            /* ignore, this Calendly event type has no details */
         }
      } else {
         /* ignore, non-Calendly event */
      }
   });

})();
Notes

[1] The event is also triggered when they merely bring up the booking widget and when they choose a date and time, though those are not so useful if you ask me.