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]
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:
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.