← Blog

What a CSP report would have shown during the British Airways breach

For 15 days in 2018, 22 lines added to a script on ba.com copied card details to baways.com. The connect-src report that names a host like that on its first request, and what CSP leaves unsolved.

Michael Cameron5 min read

On 5 September 2018, a third party told British Airways that data was leaving britishairways.com for a domain called baways.com. It had been leaving for 15 days. Every customer who paid on the website in that window had a copy of their card details sent to a server the airline had never heard of. The booking went through normally, so nothing on the inside looked wrong.

The request to baways.com is the part of this incident a browser could have reported. What the report would not have shown matters as much as what it would.

What happened

The ICO’s penalty notice sets out the sequence. An attacker used compromised credentials for a remote access gateway to reach BA’s internal network in June 2018, moved through it, and eventually edited a JavaScript file on www.britishairways.com. The edit copied payment card data to www.BAways.com, a domain the attacker owned, and stayed live from 21 August to 5 September 2018. BA’s own estimate was that 429,612 people were affected, most of them customers whose name, address, card number and CVV were taken. The ICO fined BA £20 million in October 2020, down from the £183 million it had first proposed.

BA announced the breach on 6 September and gave the window precisely: card payments made between 22:58 BST on 21 August and 21:45 BST on 5 September. Its early figure, as told to the BBC, was around 380,000 customers.

How the skimmer worked

Five days after the announcement, RiskIQ published its analysis of the modified file. RiskIQ crawled the site regularly and kept copies of its scripts, so it could diff them. The change was to BA’s own copy of the Modernizr library, version 2.6.2: twenty-two lines appended to the end of the file, a placement RiskIQ noted attackers favour because it leaves the library working.

The added code waited for the page to finish loading, then bound handlers to the mouseup and touchend events on the payment form’s submit button. When a customer pressed the button, the code serialised the payment form and the field holding the payer’s name, and sent the result as JSON to baways.com. The domain sat on a VPS in Romania and carried a paid certificate from Comodo rather than a free one, which RiskIQ read as an effort to look legitimate.

The server’s Last-Modified header dated the clean file to December 2012 and the modified one to the start of the period BA later disclosed. The mobile app was affected too, because much of it loaded pages from the website, Modernizr included.

The CSP report this would have produced

The file was on BA’s own origin, so loading it was unremarkable to a browser. The request that followed was not. A payment page carrying a report-only policy whose connect-src named only the hosts the page talks to would have sent this the first time the skimmer fired:

{
  "csp-report": {
    "document-uri": "https://www.britishairways.com/travel/book/payment",
    "violated-directive": "connect-src",
    "effective-directive": "connect-src",
    "original-policy": "default-src 'self'; connect-src 'self' https://api.example-psp.com; report-uri https://k3x9mp2nq7ab.ingest.us.headerhawk.com/r",
    "disposition": "report",
    "blocked-uri": "https://baways.com",
    "source-file": "https://www.britishairways.com/cms/global/scripts/lib/modernizr-2.6.2.min.js",
    "line-number": 21,
    "status-code": 200
  }
}

The page path and the policy are illustrative. The rest is the shape the CSP specification describes for an XMLHttpRequest, fetch() or sendBeacon() call to a host the policy does not list.

blocked-uri answers the question nobody at BA was asking: where does the payment page send data? Browsers may shorten a cross-origin value to its origin, so the path the skimmer posted to might not appear, but the host does.

source-file and line-number come from the script that made the request. Here that is the modified Modernizr file, and the line number lands inside the 22 lines appended to its end. A minified library is one long line, so a line number above 1 in a file that should have one shows something was added before anyone opens the file.

disposition says report: the request went through. A report-only policy observes. It does not intervene.

Where it surfaces

HeaderHawk groups reports by effective directive and blocked host, so every one of these would land in a single issue titled connect-src blocking baways.com, with its report count and first- and last-seen timestamps. First seen is roughly when the code went live, the fact RiskIQ had to reconstruct from a Last-Modified header three weeks later.

Opening the issue shows the individual reports, with the page each came from, the source file and line the browser attributed the request to, the policy in force at the time, and the browser and operating system. Every report would name the payment step, unlike a globally loaded SDK that violates everywhere.

Alerting is on browser agreement. Reports for an issue accumulate in 15-minute windows, and a window reported by two or more browser families, or carrying 20 or more reports from one, goes to Slack, email or a webhook, then repeats no more often than the rule’s window, which defaults to a day. A payment page on an airline’s website would clear the first window quickly.

Where the report stops

The report names the request, not the edit. script-src 'self' allowed the modified Modernizr because it was 'self', and no policy can say that a file changed. The ICO’s notice names file integrity monitoring as the control for that on the server, observing that it does not stop an attacker changing code but does let the organisation find out. Subresource integrity is its counterpart in the browser, for scripts whose contents you control.

The signal is as sharp as the policy on the payment page. A skimmer posting to an analytics host already on the allowlist, or to BA’s own origin, satisfies connect-src and sends nothing. So the payment step deserves a narrower connect-src than the rest of the site.

And the report follows the request. A report-only policy on that page would have shortened the distance between 21 August and 5 September, and the news would have come from BA’s own dashboard instead of a third party.

Sources

Read your own reports this way

Point your policy's reporting at HeaderHawk and see violations grouped by directive, blocked source and page. The Free plan covers 1 site.