At 17:56:47 ET on 2024-12-05, a bug in our license/update server caused a large number of license emails to be sent to users with an active license. The short summary is that this was not a security incident, no customer data was exposed, no extra purchases were triggered. We don’t actually have the ability to trigger additional purchases as we don’t store payment information, our credit card processor handles those details.
If you’d like more details into the timeline, what happened to cause the bug and what we’ve done to prevent it from happening again, read on!
We’ve been building out a new customer portal for several months. There are a few goals for the portal. First, we’d like to enable larger organizations to better manage their licenses, renew them in bulk, transfer between users, etc. Second, we’d like to enable users who were previous customers and have not maintained support to have access to old stable versions of Binary Ninja. Our original update server design didn’t allow users to download specific older builds and we knew that we wanted to add it at some point.
Hopefully in the next few weeks (or early 2025) we’ll be able to show the fully fledged version of this portal and it will make everyone’s life easier! (Including ours! The portal also means customers can self-service many types of transactions that currently require manual processing.)
Earlier today we pushed a change to our license server to support these changes. The change was fine during testing, right up until it… wasn’t.
But wait, we said we tested license recovery. So how did a new request for a license recovery cause the flood of emails? If a user requested to recover the licenses associated with their email, and their email didn’t have a Ninja ID associated, recovery emails would be sent for ALL licenses without an associated Ninja ID. The intended logic here is to handle the case where we are migrating users off of the purely email-based license system to the new customer portal which is backed by Ninja IDs (the same ID you use to manage your Sidekick logins). In the coming weeks you’ll be able to associate your account and your license so you can also use the portal to manage your license or download previous versions after support ends!
You can see this bug in the following simplified code:
def recover():
email = params.get('email')
...
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
user = None
...
license_selection_query = Q(email__iexact=email) | Q(user=user) # <- BUG
licenses = License.objects.filter(license_selection_query)
...
send_license_emails(licenses)
When user was None
, licenses would contain all License
objects where the email address matches, or has no associated user (which is all of them… oops.)
The fix here is obviously not to match Licenses on user
when user
is None
, or essentially changing the code to:
...
license_selection_query = Q(email__iexact=email)
if user is not None:
license_selection_query |= Q(user=user)
...
Once we’ve recovered after a good night’s sleep we’ll re-visit this and consider other longer-term mitigations we might take.
More robust testing might have caught this condition, some rate limits on the account doing the sending at the email provider level could have prevented this, we’ll see what makes the most sense with the benefit of hindsight.