A few months ago I spoke with CBC news to give an overview of a plethora of security flaws I found in PORTpass, an attempt to bring digital proof-of-vaccine to Canadians. While that article was meant for a broader audience, today I will outline their security flaws from a technical perspective for all the developers out there.
My Background
My name is Rida F’kih, I am a 21 year old Canadian software developer & reverse engineer at MaxRewards.
My development journey started at 15, from reverse-engineering to scammer-vigilantism then teaching coding to kids to web & mobile development.
What is PORTpass?
PORTpass was a private attempt by a local Calgarian entrepreneur to bring vaccine passports to Canadians.
What went wrong?
First, we must familiarize ourselves with Murphy’s Law.
Murphy’s law is an adage or epigram that is typically stated as: “Anything that can go wrong will go wrong."
— Wikipedia, Murphy’s Law
I’d argue that PORTpass was the epitome of Murphy's Law from the perspective of the user, and criminal negligence on behalf of its creator due to their inaction after these issues were brought to light.
The Data at Risk
The data at risk wasn’t light stuff.
PORTpass had the business requirement of collecting loads of personal data, collecting photos of government IDs, health card numbers, verification selfies, full names, dates of births, blood types, and more.
The data on PORTpass would be enough to be at risk of social engineering, or even have financial accounts opened in your name.
The Vulnerability
The primary vulnerability is called “Improper Access Control,” which is described by the Common Weakness Enumeration—a community-developed list of software & hardware weakness types—as “software [that] does not restrict or incorrectly restricts access to a resource from what should be an unauthorized actor.”
Some pseudo-code to describe a properly managed & created endpoint may appear as follows.
import express from "express";
import { loggedInUser } from "@/utils/authentication";
import { getUserInfo } from "@/utils/users";
const app = express();
app.get("/user/:userId", (request, response) => {
const { userId } = request.params;
const authenticatedUserId = loggedInUser(request);
if (!authenticatedUserId || authenticatedUserId !== userId)
return response.status(401).send("Unauthorized");
else
return response.status(200).json(getUserInfo(userId));
});
Here, we would send a GET request to /user/[userId]
, and receive back user data only if we are authenticated into the same user we are requesting.
A poorly architected endpoint may be coded as follows.
app.get("/user/:userId", (request, response) => {
const { userId } = request.params;
if (!loggedInUser(request))
response.status(401).send("Unauthorized");
else
response.status(200).json(getUserInfo(userId));
});
Here, we’re not checking which user is authenticated, only if the user is authenticated. Thus, an authenticated user could request ANY resource, even if it doesn’t belong to them. For public posts this is appropriate, for private information like government-issued photo IDs, health care cards, and confirmation portraits, this is a massive flaw.
The Catalysts
Sequential User IDs
PORTpass users requested their resource by using sequential user IDs. This means that if you were the first user to sign up for their service, your ID would be 1
, if you were the second, it would be 2
, etc.
Had they used a cryptographic solution—such as UUIDv4—scraping user information would have been significantly more difficult, however in this case formulating a script to collect all registered user data would be trivial, and would look something like...
/**
* Gets the user profile from the PortPass API.
* @param {number|string} userId The user ID of the target user.
* @returns {Promise<any>} A user object.
*/
const getUserProfile = async (userId) => {
const profile = await axios
.get(USER_PROFILE_URI + userId.toString())
.then((response) => response?.data)
.catch(() => ({}));
return profile;
};
for (let i = 0; i < 19577; i++)
getUserProfile(i + 1).then(saveUserData);
No SSL
Since PORTpass didn't have SSL certificates, packets that your device sends or receives could be altered or intercepted. With websites, its easy to identify a secure website by finding the little "lock icon" next to the URL, however with applications its a little more ambiguous.
Exposing Developer Tooling
PORTpass' backend was created using Django, which has an interface which allows you to test endpoints from your browser without having to use REST tooling like Postman, or Insomnia.
Optimally, PORTpass would have secured their endpoints so knowing their paths would provide no additional capabilities, however, they didn't, and thus traversing their entire backend was trivial.
Conclusion
We all want to get our projects out there, but if we don't have the experience necessary to accommodate for basic security considerations, getting your code reviewed or learning the basics of security should be considered good options.
In this case, a non-technical founder wanted to hastily get a product out there, so they hired a developer from overseas without the ability to evaluate the quality of code.
I'm very thankful to the CBC for their help in finally being able to hold the creator of PORTpass accountable, an address these concerns rather than simply ignoring them or claiming to be defamed.
You can read this article on Medium or my personal website.
If you enjoyed this content please consider following me on Twitter!
Top comments (0)