QRIS mobile banking penetration test: a case study

A state-owned bank was preparing to add QRIS payments to its mobile banking app. QRIS is Indonesia’s national QR payment standard, and the app has about four million users. Within four weeks, the bank had to pass reviews by Bank Indonesia and OJK (Indonesia’s Financial Services Authority) and get the new version approved by the app stores.

Item Detail
Client State-owned Indonesian bank, mobile app on Android and iOS
What we tested QR code creation, the mobile app, 19 backend endpoints, merchant settlement and refunds
Findings 22 in total: 1 critical, 3 high, 8 medium, 10 low
Result Critical issue fixed within five days and confirmed by retest; app released

Where QR payments can go wrong

QRIS works in two directions. In one, the merchant displays a code and the customer scans it. In the other, the customer shows a code in the app and the merchant scans it. Either way, the payment passes through the app, the bank’s servers and the settlement process that pays merchants. Each step is a place where someone could tamper with it.

The bank already had one warning. An earlier security review had flagged a secret key written directly into the app’s code.

A printed sticker was enough

The most serious finding involved the static QR codes that shops print and display at the counter. The merchant details inside the code, including the NMID (the national merchant ID used in QRIS), were not digitally signed. An attacker could print a code carrying their own merchant ID and stick it over the real one. Customers paying at that shop would then send money to the attacker.

We demonstrated this with a sticker overlay in a controlled test.

Weaknesses in the app and behind it

The app contained a hard-coded API key and did not check that it was talking to the bank’s genuine server (a control called certificate pinning). On a public Wi-Fi network, an attacker sitting between the phone and the bank could change the payment details. In testing, we changed a Rp10,000 payment into a Rp1,000,000 one.

The refund function had its own flaw. Any merchant could refund another merchant’s transaction simply by changing an ID in the request.

What the bank did about it

Dynamic QR codes are now digitally signed and expire quickly. The API key moved into a secure vault, and the app now pins its certificate. It also uses runtime protection (RASP), which detects tampering while the app is running. A retest confirmed the critical issue was closed.

The app then went through the QRIS and OJK reviews and was released to the app stores. The bank also set up a procedure for re-issuing merchant QR codes. A new rule, connected to Snipeyes Fraud Detection, flags unusual settlement patterns.

How we tested, for the technical team

  • iOS and Android static and dynamic analysis against OWASP MASVS L2, using OWASP MASTG test cases: hard-coded secrets, SSL pinning, root and jailbreak detection, intent leakage
  • QR lifecycle: MPM static print, CPM dynamic token (30-second validity), settlement callback and refund, tested for tampering, replay, race conditions and amount swap
  • 19 backend endpoints against the OWASP API Security Top 10: BOLA, IDOR on merchantId and NMID, signature bypass, rate limiting on QR generation
  • Settlement and reconciliation: merchant spoofing, double credit, webhook forgery
  • Critical: NMID and merchant PAN in the static MPM QR were unsigned; High: hard-coded SDK API_KEY with no pinning, enabling MITM payload tampering; High: refund BOLA through IDOR
  • Findings rated with CVSS and mapped to OWASP MASVS and PCI DSS 4.0
  • Carried out by CREST-certified testers, with a report within 10 days of testing and a retest

Banks, e-wallets and merchant acquirers running QRIS share these risks, from the code printed on the counter to the settlement behind it. If that describes your business, we can walk you through what a similar test would cover.