There is a category of website security you can check from outside, without permission, in about a second. When a site sends you a page it can attach instructions telling your browser how to defend you: refuse to load this page over an unencrypted connection, only run code from these places, do not let anyone put me inside a frame.
We checked those instructions on eighteen institutions across six ASEAN economies — the largest retail bank, the primary government portal and the largest mobile operator in each. The kind of site you cannot decide to use less carefully.
The headline is better than we expected. Protections run to a mean of 3.7 of 5, and five sites send all five. The problem sits one layer down. Twelve of the sites send a content security policy, and eight of them publish a policy that permits exactly the thing the policy exists to prevent.
What we looked for
| Header | What it asks the browser to do | Present |
|---|---|---|
| Strict-Transport-Security | Never speak to this site unencrypted again | 83.3% |
| X-Frame-Options / frame-ancestors | Do not let another site frame this one | 83.3% |
| X-Content-Type-Options | Do not guess what a file is | 83.3% |
| Content-Security-Policy | Only run code from places I name | 66.7% |
| Referrer-Policy | Do not tell the next site where I came from | 50.0% |
Encryption enforcement, clickjacking protection and type-sniffing protection are all near five in six. Those three are cheap: one line each, no risk of breaking the site, and most content delivery networks will add them for you.
The content security policy is the one that requires work, because it means enumerating every place your site legitimately loads code from — and it is the one where the numbers stop meaning what they appear to.
The eight policies that permit anything
A content security policy is a list of sources the browser may execute code from. Its central defence against cross-site scripting is that an attacker who manages to inject a <script> tag into your page has still not managed to get it executed, because the browser will refuse to run code that did not come from an approved source.
That defence has one well-known escape hatch: unsafe-inline. Including it tells the browser to run scripts written directly into the page — which is precisely what injected code is.
Eight of the twelve policies we found include it. Those sites have done the work of writing a policy, are shipping it on every response, and would appear as protected in any survey that counts headers. The protection against the attack the header is best known for is switched off.
This is rarely carelessness. unsafe-inline is what a policy ends up with when it is added to a site that already has inline scripts throughout, often from analytics, tag managers or a template written years earlier. Removing it means finding every one of those scripts and giving it a nonce or a hash, which is real work. A policy carrying unsafe-inline also still beats no policy, because it constrains where external scripts may load from.
Where the gaps are
Five institutions send all five headers: DBS, gov.sg and Singtel in Singapore, Maxis in Malaysia, and the Philippine government portal. Singapore's three are the only country where every slot we measured is complete.
At the other end, one site in the sample sends none of the five, and one sends only the encryption instruction.
By slot, the pattern runs against the intuition that banks would lead:
| Slot | Mean present | Encryption enforced | Has a policy |
|---|---|---|---|
| Government | 4.0 of 5 | 5 of 6 | 3 of 6 |
| Bank | 3.5 of 5 | 6 of 6 | 5 of 6 |
| Telco | 3.5 of 5 | 4 of 6 | 4 of 6 |
All six banks in the sample enforce encryption, the only sector with a perfect score on any single measure. Government portals had the highest overall header count but were the least likely to ship a content security policy, suggesting they are implementing the easy, low-impact protections and skipping the one that requires real work.
Checking any site, including your own
The following command shows the security headers for any site:
curl -sI https://example.com/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy'
Each line that comes back is a protection the site is asking your browser to apply. If a content security policy comes back, search it for unsafe-inline — that single string is the difference between a policy that blocks injected scripts and one that does not.
We have run this exercise against our own surface too — what our own security audit found reports the results, including the parts that were not flattering.
If you run a site, the ordering is uncontroversial. The three cheap headers first, because they cost a line of configuration and break nothing. Then a content security policy in report-only mode, which tells you what it would have blocked without breaking anything, and which is the only sane way to find out how much inline script you actually have. Then the work of removing it.
Three of these institutions appear in our other measurements of the same corpus. Your bank is not one domain reads their published mail records, and what the padlock actually proves applies the same habit — read what is actually being certified — to the certificate itself. All three find the same thing. The visible marker of security is usually present, and whether it guarantees what people assume is a separate question.
What a header does not prove
Three limits, and they matter more here than usual because this is exactly the kind of measurement that gets turned into a league table.
Presence is not strength. An encryption instruction says nothing about the quality of the TLS behind it. A frame protection header does not tell you whether the login flow is sound. We measured what is asked for, not what is delivered — which is why the unsafe-inline count is in this guide at all: it is the one place we could see past presence to substance.
We tested public landing pages only. Nobody logged into anyone's bank. It is entirely possible — and would be sensible — for an institution to send a stricter policy on its authenticated application than on its marketing homepage. A low score here is not a claim about the banking app.
Eighteen sites is a sample. Six countries, three slots, chosen before any result was seen. The per-slot means rest on six sites each and should be read as an indication rather than a ranking of sectors.
Both controls passed on this run — our own site scores four of five, and example.com, which sends almost nothing, scores zero — so a low reading here is a property of the site rather than of our fetching. Figures are from 19 August 2026 and the script is in our repository.