‘PORTpass’ is Not Secure.
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.
Background
My name is Rida F’kih, and I’m a Canadian software developer & reverse engineer. I started developing software at 15, from reverse-engineering to scammer vigilantism.
What is PORTpass?
PORTpass was a private attempt by a local Calgarian entrepreneur to bring vaccine passports to Canadians.
The Data at Risk
PORTpass had the business requirement of collecting loads of personal data including photos of government IDs, health card numbers, verification selfies, full names, dates of births, blood types, phone numbers, and more.
If you were a PORTpass user at the time of the vulnerability, a bad-actor could open financial accounts in your name, or attempt social engineering attacks with the plethora of personal information they had access to.
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.
Inaction
Upon learning about these security vulnerabilities, rather than putting together an action-plan, pulling the service for maintenance, Zakir Hussein, founder of PORTpass immediately became defensive, denying private user data was openly available despite numerous reports.
Lack of Technical Knowledge
The creator of PORTpass did not have any technical knowledge, so they outsourced the work to a freelance developer in Pakistan as per the profile linked to the Expo build files & network requests. Without the ability to vet the codebase himself, any insecure code would make it to their production environments, just as it did.
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.
I’m very thankful to the CBC for their help in finally being able to hold the creator of PORTpass accountable, forcing his hand to address these concerns rather than simply ignoring them or claiming to be defamed.
If you enjoyed this content please consider following me on Twitter!
Written with ❤️ by Rida F'kih