Case study · Client-side and payment security

Every script runs as the user: locking down third-party code on an Indonesian e-wallet's web app

An Indonesian e-wallet's web app loaded a long list of third-party scripts for analytics, chat, performance and marketing, all running with the signed-in user's authority on pages that could move money. When a hijacked performance script drained traders on another platform in October 2026, the team asked the obvious question: what could our scripts do with our users' sessions? The answer led to a new front-end architecture.

Case studyFintech · IndonesiaSumanta Mukhopadhyay9 min read
#security#architecture#platform
CASE STUDY · CASE STUDY · CLIENT-SIDE AND PAYMENT SECURITY2 Nov 2026 · 15:55 GST
At a glance
SectorFintech · Indonesia
My roleFractional CTO and principal architect
EngagementAssessment, architecture and delivery leadership
ScopeScript inventory, backend-for-frontend, Content Security Policy, integrity checks, step-up confirmation, monitoring
What changed
  • No third-party scripts on pages that can move money
  • Remaining vendor scripts self-hosted, pinned and integrity-checked
  • Browser talks only to the e-wallet's own backend-for-frontend
  • Transfers and withdrawals need fresh confirmation, not just a live session
  • Unexpected script or header changes on sensitive pages raise alerts
Backend-for-frontend (BFF)Content Security Policy with reportingSubresource IntegritySelf-hosted vendor scriptsServer-side analytics eventsStep-up authenticationPayment page change monitoringOpenTelemetry

Anonymised composite. Client names, identifying details and confidential figures are withheld; public facts are cited in the references.

The situation

An e-wallet in Indonesia served users through a mobile app and a web app. The web app let users top up, pay merchants, transfer to bank accounts and withdraw. Over the years, marketing, product and support had added scripts from analytics providers, a tag manager, a chat widget, a session recorder, a performance monitor and several advertising pixels. Most loaded on every page, including the transfer and withdrawal flows, and several were loaded directly from vendors' servers.

Why now. On 29 October 2026, malicious code replaced a third-party performance script loaded by Polymarket's website and used signed-in traders' sessions to withdraw funds; Polymarket put the loss at about $2.7 million12. No passwords or keys were needed: the script simply acted with the access the open page already had1. The card side of the business was already subject to PCI DSS v4 requirements that scripts on payment pages be authorised and protected from tampering, and that changes to payment pages be detected3, and Indonesia's Personal Data Protection Law applies to the personal data those scripts could see4. The board wanted the risk removed, not just documented.

What had to be true

  • No third-party code on money-moving pages.
  • Every remaining script known, justified and integrity-checked, served from the e-wallet's own domain.
  • The browser holds as little power as possible: no long-lived tokens, no direct calls to vendors.
  • A live session is not enough to move money: transfers and withdrawals need fresh confirmation.
  • Changes are noticed: unexpected scripts or header changes on sensitive pages raise alerts.

Architecture

Browser
Sensitive pagesfirst-party code onlyOther pagesapproved scripts, self-hosted
Browser controls
Content Security Policyallow-list, with reportingSubresource Integritypinned script hashes
Backend-for-frontend
BFFsessions and tokens server-sideServer-side eventsanalytics without browser tags
Money movement
Step-up confirmationPIN or biometric per transferLimits and velocity checksper user, per device
Detect
Payment page monitoringscripts and headersCSP violation reportsalertsTracingOpenTelemetry
Remove third-party code where money moves; contain it everywhere else; require fresh intent for every transfer.

The data flow, end to end

  1. LoadThe transfer page loads only first-party scripts under a strict policy
  2. RequestThe browser calls the e-wallet's backend-for-frontend, never a vendor directly
  3. ConfirmThe user confirms the transfer with a PIN or biometric, bound to the amount and recipient
  4. CheckLimits and velocity rules run server-side before execution
  5. ExecuteThe transfer runs; analytics events are sent server-side, not from the page
  6. WatchMonitoring compares the delivered page and headers with the approved version
A malicious script on another page cannot complete this flow without the user's fresh confirmation.

The hard part: taking scripts away from the teams that rely on them

Every script had an owner who needed it: marketing measured campaigns, support used the chat widget, product watched session recordings. Simply deleting them would have failed politically and practically. The team ran an inventory first, recording for each script what it did, who owned it, which pages it loaded on and what data it could read. Many turned out to duplicate each other or to have no current owner.

The bigger change was architectural. The web app moved to a backend-for-frontend: the browser talks only to the e-wallet's own server layer, which holds session tokens and calls internal services and vendors on the user's behalf. The OpenTelemetry demo's front-end shows the same shape: client-side code calls the app's own API routes, which then call the backend services5. With that in place, analytics and marketing events could be sent server-side, so most tags no longer needed to run in the browser at all.

Front-end data flow diagram: inside a Next.js application, client-side rendering calls REST endpoints on the app's own API routes, which call cart, product catalogue, recommendation and other backend services over gRPC
The browser calls the app's own API layer, which calls everything else: the backend-for-frontend shape that keeps tokens and vendors off the page. Source: OpenTelemetry blog, Oscar Reyes · CC BY 4.0

For the scripts that remained, such as the chat widget on help pages, the team self-hosted pinned versions with integrity hashes, so a change at the vendor could not silently change the code running in users' browsers, and listed every allowed source in a Content Security Policy with violation reporting turned on. Vendor updates became a deliberate change, reviewed like any other dependency.

Treat every third-party script as a contractor with your user's keys. Keep contractors off the floor where money moves, and check the ones you keep every time they change.

How it ran

Security
  1. Script inventory
  2. CSP and integrity rules
  3. Page monitoring
Engineering
  1. Backend-for-frontend
  2. Step-up confirmation
  3. Server-side events
Marketing, support, product
  1. Justify each script
  2. Move to server-side data
  3. Approve vendor changes
Every script got an owner, a reason and a home; most moved off the page entirely.
Week 1Contain
  • Strip scripts from transfer pages
  • Step-up on withdrawals
  • Lower limits for new devices
Weeks 2 to 6Inventory and policy
  • Script register
  • CSP in report mode
  • Self-host kept scripts
Weeks 7 to 12Re-architect
  • Backend-for-frontend
  • Server-side analytics
  • CSP enforced
OngoingWatch
  • Page change monitoring
  • Violation alerts
  • Vendor change reviews
Week one removes the biggest risk; the quarter removes the cause.

Key decisions and trade-offs

DecisionAlternativeWhy
No third-party scripts on money pagesMonitor them closelyRemoval beats detection where money moves
Backend-for-frontendBrowser calls services and vendors directlyTokens and integrations stay server-side
Server-side analyticsBrowser tagsSame insight, far less code in the page
Self-hosted, integrity-checked scriptsLoad from vendor serversVendor compromise cannot change running code
Fresh confirmation per transferTrust the active sessionA hijacked session alone cannot move money

What I'd do again, and differently

Again: strip the money pages in week one. It removed most of the risk before the longer re-architecture began. Differently: run the Content Security Policy in report-only mode from the first day of the inventory; its reports found scripts that nobody had listed. Related: architecture lessons from the Binance hack, the 2 November tech news, securing AI coding tools and where I work.

If you run a wallet, marketplace or fintech in Indonesia or across Southeast Asia, see the Singapore and Southeast Asia page, or analyse your challenge and bring the result to a call.

References
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
FAQ

Common questions

Why are third-party scripts dangerous on payment pages?

A script loaded into a page runs with the same access as the page itself. If a vendor's script is replaced with malicious code, it can act with the signed-in user's session, as happened to Polymarket traders in October 2026, without needing passwords or keys.

What do PCI DSS requirements 6.4.3 and 11.6.1 require?

In summary, 6.4.3 requires that scripts on payment pages are authorised, justified and protected from tampering, and 11.6.1 requires detecting unauthorised changes to payment pages and their security-relevant headers. Confirm scope and detail with your assessor.

What is a backend-for-frontend?

A server-side layer owned by the app that the browser talks to instead of calling many services and vendors directly. It keeps tokens and vendor integrations on the server, so less code in the browser can act for the user.

Keep reading

Related

Open to advisory, fractional CTO & select builds

Let's talk about what you're building.

The fastest route is a free 30-minute call. Prefer to write? Send a message and I'll reply personally within two business days.

Start a conversation

How would you like to reach me?

Book a 30-min callFastest. Pick any open slot on my calendar.→ Write in GmailOpens a pre-filled draft in your browser.→ Use my email appOutlook, Apple Mail or your default client.→

I reply personally within two business days. Time zones from the Americas and Europe to the Middle East and Asia-Pacific.