Addressing the Security Flaws in Malaysia's New PADU Central Database Site : An Experience
Yesterday, the government of Malaysia officially launched a digital initiative "PADU" (Pangkalan Data Utama = Central Database Site), which aims to serve as a platform for every citizens of Malaysia to store/report their personal details, such as educational and employment backgrounds, household status, disabilities, tax reliefs. It is said to be used as a reference for the government to better adjust and tailor the subsidy towards the desired groups.
Upon launching, the Ministry of Economy (Ministry in Charge) urges every citizens of Malaysia to promptly register at the website and perform user verification to begin submitting their details. The website is launched at the URL padu.gov.my.
Hours into the launching, a member of parliament, Dr. Ong Kian Ming raised a concern regarding the registration flow of the website. A registration only requires a verified National Register Identity Card (NRIC) number & postcode, and an active local Malaysian phone number to receive the TAC. Only after the initial registration, a proper image identification of the NRIC and realtime facial recognition ("e-KYC") is performed to verify the user. This leads to a concern that a third party user may register someone else's NRIC with their own phone number.
First Look At The Site
I was only able to take a look at the website in the evening. Registration flow did look a bit concerning. Upon logging in at the first time, even without e-KYC, i could already view some of my personal info, such as my employment and education history. An attempt at e-KYC was failed, but I assumed because I didnt do it properly (the image and photo i submitted were suboptimal and full of glare).
Peeking under the hood with Wappalyzer revealed that the site is powered by Next.js (React) on the frontend and Tailwind as UI framework. A standard collection of static JS files generated by Next.js can be accessed via the Browser Inspector. Immediately in the script, the sign-in authentication route can be seen via calling an API with NRIC number and the password as JSON payload. Both values are not encrypted, the API route is exposed and no authentication headers were set.
I tried making a call to the api/auth/login
route by passing an empty string of noKp
and katalaluan
. The server responded with failed status and error message "Kata laluan tidak memenuhi syarat."
(The password does not meet the requirement) . Hmm.. funny. Another call to the route were made, with a proper random password, returning a message "No Kad Pengenalan tidak sah"
(Invalid NRIC Number).
I tried making few other calls to the API. Upon using a wrong password, it returned a success 200
status but with data value "Unauthorized".
Finally, I tried calling the API using my NRIC Number and the correct password. It returned a success 200
status with response "Authenticated"
msg, an UUID, my full name, my own NRIC, and a JWT token.
The JS source code reveals that upon a successful call with "Authenticated"
msg, will save the JWT token into the session_storage
of the browser and redirects the user to /page/data
page route. This page route is protected and uses the JWT token for subsequent API calls.
Taking a Jab at the Unprotected Routes
There are two unprotected page routes on the site, the /auth/daftar
and /auth/terlupa-kata-laluan
. The JS source codes of both routes revealed several other API endpoint URLs, that is similar to the front page : unprotected, without Authorization header, and unencrypted JSON payload. Both routes implement an almost similar frontend API flow,
Call
/api/daftar/maklumat-asas
route with the user's particular, will prompt server validation and if succeed, returning an UUID. In/api/auth/terlupa-kata-laluan
, the API requiring only NRIC number and phone number, returning UUID.Call
/api/daftar/hantar-otp
route with UUID and NRIC Number, prompting an SMS OTP sent to the supplied phone number in (1).Call
/api/daftar/validate-otp
route with NRIC number and OTP Key, returning either invalid or success message response.
Once the last API is called and returned as success, indicating that the NRIC Number is registered and UUID is returned, the script loads the screen for setting a new password. This is where I found a critical error.
The API route for setting a password ( /api/auth/terlupa-katalaluan/reset-katalaluan
& /api/daftar/set-katalaluan
) only requires one "noKp
" string value, one "Password
" string value, and one "Type
" string value.
My first thought was that probably the developer assumed on the frontend, the user would not reach this API call unless they passed the OTP verification in the previous screen. When in fact, the API request method can be seen in the script regardless of the OTP verification.
I attempted a call to the API using my own NRIC number using a random password "Password01
", and to my surprise, the server returns a success 200
status with a "success
" msg.
And yes, my password has been changed, and I can no longer login via frontend with my old password.
At first, I thought the "Type
" value is required to validate the call, that perhaps it is a token, or at least the "Postcode
" information from the NRIC. But several attempts with random strings/numbers as "Type
" value returns successfully.
I contacted a friend who also has registered to the website, and asked his permission to test with his NRIC number. I made a similar call to the API, and successfully changed his password. I subsequently was able to login using his NRIC Number and the changed password, on my computer.
Alerting The Authorities
Knowing that this is a very critical error, and that currently at that time tens of thousands of Malaysian citizens has already registered to the site, I raised a post on my X account, tagging a fellow developer friend, alongside the Minister of Economy himself, his officer, and the Chief Statistician of Malaysia.
Within a couple of hours, I could see that they have made some changes to the script. Initially the critical API call was enhanced by requiring UUID instead of NRIC Number. A few hours later, the OTP Code value is also required to call the API.
Currently, the developers have mitigated the issue by making those changes. Users are no longer able to call the reset password API unless they have the UUID, the sent OTP code to the registered number, and the NRIC Number. While it is not perfect, I'm sure they will continue taking a deep look into this issue.
My post on X gained tractions and attentions of multiple parties. Public and netizens were expectedly outraged, as they perceived that the website and the whole initiative as not yet ready for public usage. Developer community and cybersecurity experts raised concerns whether the site has been properly tested, reviewed and validated prior pushing to production. Some others shared their opinion and suggestions in making the site more secure and up to the standard in security viewpoint.
The next day, the official X account of the Ministry of Economy and the Chief Statistician of Malaysia himself express a gratitude to me and all the other developers that contributed in the suggestions.
As for myself, I am actually glad that I found the critical error and managed to alert the respective parties in time. I can only imagine how disastrous it could be, especially when more citizens of Malaysia have not only registered to the site, but performed their e-KYC verification and submitted their data. Their login details and passwords could be easily changed and their account could be accessed by a third party, leading to probably the biggest data leak Malaysia would ever had.
What Would I Have Done Differently
All API Endpoint URL should not be exposed. If it is needed to be exposed, or intentionally for public use, it should come with API Key, rate limitation, cors-header restriction.
Password information, at all time, should be encrypted hashed. A simple sniffing to the network packet could expose an unencrypted data.
Personalized details, in this case the "NRIC Number", should not be part of the key/identifier for any requests. Use UUID for all transactions.
Correct API status responses. To make use of all the other API status responses and handling them differently. Returning a
200 Success
status with an "Error" message, makes no sense.
Based on what happened, I assume the developers, specifically the frontend and backend teams, perhaps did not communicate or work together (or simply not given enough time to collaborate) and assume that the other party would done the validations. This experience teaches me the importance of working in a team, collaboration, and crowd sourcing.
On a personal note, actually I'm not that fond on Information Security, or the Blue-Red-Purple team. It is not in my interest, nor it is my forte. I consider myself more of a builder/creator, or perhaps, the "Yellow" team.
Signing off now.