British Airways: the payment script nobody watched
22 added lines on a payment page, 15 days of copied cards.
At 22:58 UK time on 21 August 2018, British Airways’ booking pages started copying the card details customers typed and sending them to a second address. The code that did it was 22 lines long. It ran on ba.com and in the BA app until 21:45 on 5 September, and BA didn’t spot it. An outside party told the airline.
The UK Information Commissioner’s Office later counted 429,612 customers and staff whose data the attacker may have reached. In October 2020 it fined BA £20m, its biggest fine to that date.
What happened
It started two months earlier. On 22 June 2018 the attacker logged in to BA’s Citrix remote access gateway with a username and password BA had issued to an employee of Swissport, a cargo contractor, in Trinidad and Tobago. That login asked for nothing more than the password.
Once inside, the attacker broke out of the Citrix environment and found the password of a domain administrator account in a plain‑text file. On 26 July the attacker reached log files that held payment card details in plain text, left there by a testing feature nobody had switched off. About 108,000 cards sat in those logs.
Then the website. Between 14 and 25 August the attacker edited a JavaScript file on britishairways.com. RiskIQ, a security firm that crawls websites, later identified it as BA’s copy of Modernizr 2.6.2, an open‑source library, with new code added at the bottom. When a customer pressed the pay button, that code read the payment form and the payer’s name and posted them to baways.com, a domain the attacker controlled. The app loaded the same web pages, so app bookings leaked too. The booking itself went through as normal.
The skimmer stayed live for 15 days. On 5 September a third party told BA that data was going to BAways.com. Within 90 minutes BA had neutralised the code, and 20 minutes later it blocked the paths to BAways.com. BA went public on 6 September with a first count of about 380,000 transactions.
In July 2019 the ICO said it intended to fine BA £183.39m. The final penalty, issued on 16 October 2020, was £20m, after BA’s representations and an allowance for the effect of Covid‑19 on its business.
Why it worked
Two failures sat on top of each other.
The way in was a password and nothing else. BA’s own access policy, written in October 2017, required multi‑factor authentication for all remote access, contractors included. The Swissport account didn’t have it.
The way out was a file nobody watched. BA had an approval process for staff who wanted to change the website, but it couldn’t point the ICO to any technical or organisational measure that would catch an unapproved change to the website code. A browser runs whatever script the page hands it, and that script can read every field on the form. Nothing on the page said where the data was allowed to go, so nothing stopped a request to a lookalike domain.
Could a scan have seen it?
Partly. An outside check reads what any visitor’s browser receives, and RiskIQ counted about 30 scripts on BA’s booking page. Three of the scan’s checks bear on this: “Scripts from external domains”, “External scripts without an integrity check” and “No Content‑Security‑Policy header”.
An integrity hash is a fingerprint the browser compares before it runs a file, and it refuses a file that no longer matches. The altered Modernizr came from BA’s own domain, though, so the two script checks wouldn’t have pointed at it. The policy matters most here. One that allowed only BA’s own domains would have told the browser to refuse a request to baways.com. Neither is a wall: an attacker who can edit a file on the server may be able to edit the hash and the policy too.
What no outside check sees is the start of the chain. The contractor’s password, the missing second factor and the plain‑text admin password were all inside BA’s network. A scan also can’t read intent. The skimmer was custom‑built, so RiskIQ’s own blocklists had no match. RiskIQ found the file only by comparing each script with its earlier copies.
What to do
- Add an integrity hash to every script your checkout loads from another domain, so a browser refuses a file that has been changed.
- Set a Content Security Policy that names the domains your pages may load scripts from and send data to.
- Strip your payment pages down to the scripts that take the payment, and drop chat widgets, trackers and anything that can wait.
- Turn on a second factor for every admin login and every remote access account, including the ones you give contractors.
The scan checks the first two from outside.