# RoastMyOpsec
> Paste a URL. Get a ruthless OPSEC roast: free no-brainer insights, 20+ deep findings, score 1–10, and a public leaderboard.

RoastMyOpsec is a consent-based public OPSEC roast. Paste a URL you own or have permission to scan. Private IPs and localhost are blocked. No exploit payloads. The free roast covers public headers, TLS, cookies, well-known paths, and JavaScript secrets. The paid vault adds deeper findings, remediations, and a Discord month. Do not treat robots.txt Disallow or this file as a vault — neither authenticates anyone.

Index of links: https://roastmyopsec.com/llms.txt
Human blog index: https://roastmyopsec.com/blog

# Product

RoastMyOpsec fetches publicly reachable responses for a URL the visitor submits (headers, HTML, linked same-origin scripts, selected well-known paths, public DNS). It does not send exploit payloads. Free findings stay on the report; the vault unlocks the rest. Private IPs, localhost, and link-local targets are rejected.

# News

## X-Robots-Tag vs Meta Robots for Public Sites

- URL: https://roastmyopsec.com/blog/x-robots-tag-vs-meta-robots
- Category: News
- Updated: 2026-08-13

X-Robots-Tag vs meta robots for public sites is a delivery-channel decision: the HTTP header works on HTML and non-HTML, while the robots meta tag only exists on HTML documents. Pick the header for PDFs, images, and CDN-served files; pick the meta tag when the document template already owns SEO. Neither is access control, and RoastMyOpsec does not score missing robots directives as a standalone F.

### The practical difference

The robots meta tag lives in HTML and is ignored on a PDF or a bare JSON response. X-Robots-Tag is the same vocabulary (noindex, nofollow, nosnippet, and friends) on the HTTP response, so a CDN can mark a download without wrapping it in a page. Google Search Central documents both as equivalent signals when they apply.

Crawlers that never fetch the body will not see a meta tag. Crawlers that never look at headers will not see X-Robots-Tag. Well-behaved search crawlers look at both. Attackers and random clients do not have to. A noindex header does not replace auth, edge deny, or keeping backups off the origin.

Conflicting signals — meta index plus header noindex, or robots.txt Disallow that blocks the crawler from seeing noindex — are how pages linger in a surprise index. Resolve the conflict at the edge instead of stacking folklore.

### How to choose based on what you shipped

Ship X-Robots-Tag on non-HTML assets and on HTML if the edge already injects headers. Use meta robots when marketing CMS templates are the source of truth and you cannot touch response headers. Do not rely on either to hide a path that should return 401/403 or not exist.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| X-Robots-Tag | PDFs, images, or CDN files need noindex | Easy to forget on one cache layer | Header works without HTML |
| Meta robots | The HTML template already owns SEO | Does nothing on non-HTML responses | Fine for documents only |
| robots.txt Disallow | You want well-behaved crawlers to skip a prefix | Can prevent crawlers from seeing noindex | Guidance, not a lock |
| Authentication / deny | The URL should not be public at all | Indexing tags are not a substitute | This is the actual control |

### When X-Robots-Tag still wins

X-Robots-Tag still wins for attachments, print PDFs, and any response that has no <head>. Meta robots still wins when the CMS cannot set headers but can edit the document. Missing either is normal on a brochure homepage you want indexed.

### Common mistakes

The first mistake is putting noindex only in HTML on a PDF that search engines still fetch as a file.

The second mistake is Disallowing a URL in robots.txt so crawlers never see the noindex you added.

The third mistake is treating X-Robots-Tag as a substitute for access control.

### What a URL roast can prove

RoastMyOpsec’s free pass covers headers (CSP, HSTS, framing, referrer, nosniff, Permissions-Policy), TLS, cookies, path probes such as .git/HEAD and .env, package.json signatures, and js-secrets. It does not grade X-Robots-Tag or meta robots as a standalone F. Confirm crawl hints in your own crawler tools; use the roast for origin leaks.

### FAQ

**Can meta robots noindex a PDF?**

No. PDFs have no HTML meta tag. Use X-Robots-Tag on the file response, or do not expose the file.

**Is noindex a security control?**

No. It is a hint to cooperating crawlers. Anyone who can request the URL can still read it.

**Does RoastMyOpsec fail missing robots tags?**

No. Crawl directives are not a scored F. The roast still reads other response headers that actually affect browsers.

---

## change-password well-known vs a Custom /reset URL

- URL: https://roastmyopsec.com/blog/well-known-change-password-vs-reset-url
- Category: News
- Updated: 2026-06-23

change-password well-known vs a custom /reset URL is a pointer versus the actual form. WICG / RFC 8615 well-known URIs put a redirect at https://example.com/.well-known/change-password so browsers and password managers can find 'change password' without guessing /account/security. The destination still needs session auth, CSRF controls, and no reset tokens in the URL. Missing the well-known path is a convenience gap, not an OPSEC F. A reset link with the token in the query string is the miss — see open redirects vs tokens in URLs. RoastMyOpsec does not require change-password. It may probe other well-known sensitive paths for signatures. It does not submit password-change forms.

### The practical difference

The well-known URL is a stable address. Your product URL can move; the well-known entry redirects. /reset, /forgot, and /wp-login.php?action=lostpassword are product paths. Password managers look up well-known first.

security.txt is for researchers. change-password is for users' password tools. Do not mix the two files.

### How to choose what to publish

If you have a logged-in change-password page, add a 302 from /.well-known/change-password to that HTTPS URL (same site). If you only have email reset, point at the reset request page — not a URL that already contains a token. If you have no accounts, skip the file.

| URL | Audience | Must not contain | Takeaway |
| --- | --- | --- | --- |
| /.well-known/change-password | Password managers | Secrets; open redirects off-site | Redirect to the real form |
| /account/password | Logged-in humans | The old password in a GET | CSRF + session + HTTPS |
| /reset?token= | Email click | Long-lived tokens in Referer logs | See tokens-in-URLs guide |
| /.well-known/security.txt | Researchers | VPN hostnames | Different well-known; see security.txt |

### What the roast will not do

No password-change POST, no token redemption. A 404 on change-password is not a finding. An open redirect from well-known to another registrable domain would be a trust-boundary issue if we followed it — keep the hop on your host.

### Common mistakes

The first mistake is well-known pointing at http://.

The second mistake is putting the reset token in the well-known Location.

The third mistake is treating a missing change-password file like a missing CSP.

### FAQ

**Do I need /.well-known/change-password to pass a roast?**

No. It is a password-manager convenience. The roast does not score it as a vulnerability.

**Can change-password be a 200 HTML page instead of a redirect?**

A redirect to the real UI is the usual pattern. A 200 is fine if that page is the form and stays on your origin.

**Is this the same as security.txt?**

No. security.txt is disclosure contact. change-password is where users change credentials.

---

## Website OPSEC vs App OPSEC: Which Scan Should You Run?

- URL: https://roastmyopsec.com/blog/website-opsec-vs-app-opsec-scan
- Category: News
- Updated: 2026-06-06

Website OPSEC vs app OPSEC is a scan-mode choice on the same public URL, not two different products. Website mode emphasizes marketing-site hygiene: headers, cookies, trackers, robots.txt, mixed content. App mode leans into API and auth discoverability, CORS, HTTP methods, and chatty JSON errors. Most teams with a brochure site plus a product origin should run website mode on the marketing host and app mode on the app host. RoastMyOpsec still only reads public responses — no exploit payloads, no private-network targets.

### The practical difference

A marketing site is mostly documents, tags, and CDN rules. Failures look like missing CSP, soft cookies, HTTP assets, and a robots.txt that lists /admin.

An app origin is mostly APIs and sessions. Failures look like reflected CORS, TRACE advertised on OPTIONS, GraphQL introspection left on, and error bodies that include stack traces.

The URL you paste still has to be yours. Localhost and private IPs are blocked.

### How to choose based on what you shipped

If customers land on a Next.js or Webflow marketing domain, start with website mode. If they authenticate on app.example.com or api.example.com, run app mode there. If one host does both, run the mode that matches the risk you care about this week, then the other.

| Surface | Better starting mode | Why | Takeaway |
| --- | --- | --- | --- |
| Marketing site / blog | Website | Headers, cookies, trackers, mixed content, robots | This is the public costume of the brand |
| SaaS app origin | App | Auth paths, CORS, methods, JSON errors, GraphQL | Still a public URL — not an authenticated pentest |
| Docs + status + app on one host | Website first, then app | Document OPSEC is cheaper to fix at the edge | Do not skip APIs because the blog looks clean |
| GitHub-connected product | Either URL mode, then deepen | Repo secrets and Actions are not visible from HTTP alone | OAuth deepen is optional and scoped |

### What both modes refuse to do

Neither mode is hacking. There is no password spraying, no payload injection, no fuzzing. Findings are evidence from headers, HTML, bounded path probes, DNS, and optional GitHub reads you authorize.

That also means a clean scan is not a pentest report. It is a ruthless public-surface roast.

### News you can use this week

Teams keep splitting brand sites and apps across hosts, then hardening only the app. Attackers and reporters still screenshot the marketing domain. Scan both. Publish the free score if you want the leaderboard shame; unlock the vault if the blurred findings look expensive.

### Common mistakes

The first mistake is scanning the CDN apex and ignoring www or the app subdomain.

The second mistake is treating a 10/10 marketing scan as app security.

The third mistake is pasting a URL you do not own.

### FAQ

**Should I run website or app mode?**

Website mode for brochure and content hosts. App mode for origins that serve APIs and login. Many products need both on different hostnames.

**Is this a penetration test?**

No. It is a defensive, consent-based audit of public responses for a URL you assert you control.

**Does GitHub replace the URL scan?**

No. GitHub deepen adds repo-level findings the URL cannot see. Start with the public URL.

---

## Webmention vs Pingback for Public Blogs

- URL: https://roastmyopsec.com/blog/webmention-vs-pingback-for-public-blogs
- Category: News
- Updated: 2026-05-29

Webmention vs Pingback for public blogs is a notification-protocol decision: Webmention (W3C) is a simple HTTP mention flow, while Pingback is an older XML-RPC handshake still enabled on many WordPress origins. Prefer Webmention if you want IndieWeb notifications; disable Pingback if you do not have a reason to keep XML-RPC open. RoastMyOpsec does not score Webmention or Pingback discovery as a standalone F, and neither protocol is consent to scan someone else’s site.

### The practical difference

Pingback sends an XML-RPC method when another site thinks it linked to you. It historically needed xmlrpc.php reachable. Webmention is a POST of source and target URLs after discovering a rel=webmention endpoint, specified by the W3C. Both can be abused as unsolicited traffic; both can be useful for a blog that wants a public conversation.

The OPSEC difference is blast radius. XML-RPC on WordPress is a wider surface than mention notifications. If you do not need remote publishing or pingbacks, turning that endpoint off is ordinary hardening — not a how-to for attacking it. Webmention endpoints should verify that the source actually links to the target before displaying anything.

Unsolicited probing of other people’s blogs “to test pingback” is not research. Scan URLs you own. Mentions are not a vulnerability disclosure channel; that is security.txt.

### How to choose based on what you shipped

If you run a static or IndieWeb-aware blog and want notifications, advertise Webmention and verify sources. If you run WordPress and do not use Pingback, disable it and do not leave xmlrpc.php as a public toy. If you want neither, omit the link headers and live a quiet life.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Webmention | You want W3C mentions with URL verification | Displaying unvalidated source HTML | Prefer this if you want mentions |
| Pingback | A legacy network still depends on it | XML-RPC surface you did not mean to keep | Disable if unused |
| Neither | A brochure or docs site with no conversation | Plugins re-enabling xmlrpc.php | Healthy default |
| security.txt | You want researchers to reach a mailbox | Putting Contact only in a mention endpoint | Different job than mentions |

### When Pingback still wins

Pingback still wins only when a specific community still sends it and you have mitigated XML-RPC to what you actually need. Webmention still wins for new mention support. Missing both is normal. Leaving unused XML-RPC enabled is the blog-platform miss, not a roast F for absent rel=webmention.

### Common mistakes

The first mistake is leaving Pingback XML-RPC enabled because a default theme advertised it.

The second mistake is rendering Webmention source pages without checking the link exists.

The third mistake is treating a missing Webmention endpoint as a security finding.

### What a URL roast can prove

RoastMyOpsec is a consent-based roast of a URL you own: headers, TLS, cookies, path probes (.git/HEAD, .env, package.json signatures), and js-secrets. It does not score Webmention, Pingback, rel=me, JSON-LD, or RSS as standalone Fs. It is not a license to ping other people’s xmlrpc.php. Confirm mention endpoints yourself if you ship them.

### FAQ

**Should a new blog enable Pingback?**

Usually no. Prefer Webmention if you want mentions, or neither. Treat unused XML-RPC as extra surface.

**Is Webmention a security.txt replacement?**

No. Mentions are public conversation. Researcher intake belongs in RFC 9116.

**Does RoastMyOpsec test pingbacks?**

No. It does not send Pingback or Webmention payloads and does not score those endpoints as Fs.

---

## WebAuthn vs Passwords for Public Site Login

- URL: https://roastmyopsec.com/blog/webauthn-vs-password-for-public-login
- Category: News
- Updated: 2026-05-20

WebAuthn vs passwords for public site login is an authenticator-bound credential versus a string people reuse. Passkeys (WebAuthn / FIDO2) prove possession of a device-bound key, often with a biometric or PIN on that device. They cut credential stuffing and many phishing forms that only steal a password field. They do not replace TLS, rate limits, or access control on /wp-admin. A brochure site with no login does not need them. RoastMyOpsec does not click Sign in, does not touch WebAuthn, and does not score missing passkeys as an F. It may still grade login discoverability and cookie flags on the URL you paste.

### The practical difference

A password is a shared secret that leaks in dumps and support chats. WebAuthn is origin-scoped: the browser will not silently use a passkey for a lookalike host. SMS OTP is a third costume — better than password-only, worse than a passkey for phishing. See rate limits vs captcha and change-password well-known.

Asset links and apple-app-site-association matter if the passkey should work in an app. That is a different public file.

### How to choose an auth plan

No accounts: skip passkeys. Marketing plus a newsletter: magic links or a host you actually harden, not a leftover WordPress login. Real accounts: offer passkeys, keep a recovery path that is not 'email us the password,' rate-limit the remaining password or OTP fallback. Do not leave XML-RPC on as a password oracle. Recovery codes belong in the user's hands, not in a public gist.

| Method | What it proves | Public site | Takeaway |
| --- | --- | --- | --- |
| Password only | Knowledge of a string | High stuffing cost | Add rate limits; prefer passkeys |
| WebAuthn / passkey | Device-bound key for this origin | Best for real accounts | Not a roast F if missing |
| SMS OTP | Possession of a number | SIM-swap and phishing cost | Fallback, not the only factor |
| change-password well-known | Where to rotate a password | Optional if you still have passwords | See change-password vs reset URL |

### When passwords still win (as a fallback)

A carefully rate-limited password plus passkeys still wins for users who lost the device. Passkeys win as the default for new accounts. Missing WebAuthn on a brochure is healthy. Pair with admin discoverability if /wp-login.php is the real story.

### What the roast can prove

Login path discoverability and cookie flags on a consented GET. WebAuthn ceremonies are out of scope. Confirm passkeys in your own staging login. Pair with consent-based scanning — do not roast third-party logins as research.

### Common mistakes

The first mistake is passkeys on a marketing host whose real login is a different origin with no related origin binding.

The second mistake is a password fallback with no rate limit because 'everyone uses passkeys now.'

The third mistake is treating missing passkeys as a header-scan F.

### FAQ

**Is a password-only login a vulnerability?**

It is a weaker authentication choice, especially without rate limits. It is not an automatic roast F. Passkeys are the upgrade for sites that have accounts.

**Do brochure sites need WebAuthn?**

No. If there is no login, skip it. Missing passkeys is not a roast finding.

**Does RoastMyOpsec test passkeys?**

No. It does not run WebAuthn ceremonies or score missing passkeys.

---

## Web App Manifest vs Public Metadata on Production

- URL: https://roastmyopsec.com/blog/web-app-manifest-vs-public-metadata
- Category: News
- Updated: 2026-05-12

Web app manifest vs public metadata on production is a PWA catalog versus other JSON you accidentally ship. /manifest.webmanifest or /manifest.json names the app, icons, start_url, and display mode so installing the site works. It is fetched by browsers without a login. API keys, staging hostnames, and admin start_url values do not belong there. package.json on a public origin is a different leak (dependency map). assetlinks.json proves which native apps may open your URLs. RoastMyOpsec does not score the web app manifest. It may signature-check public package.json. Missing a manifest is a PWA gap, not an OPSEC F.

### The practical difference

The manifest is advertising: name, theme color, icons. start_url should be a public HTTPS path, not /reset?token= or /admin. related_applications can list store IDs — that is fine; do not list internal package names you would not print on the homepage.

scope and id should not point at a second origin you do not control. That is an open-redirect-class product bug if start_url can leave your site.

### How to choose what to publish

Production brand only. Staging manifests on staging hosts. Icons without EXIF leftover paths. start_url: the marketing or app shell you already show logged-out users. Prefer a static file with a boring Content-Type. Do not generate the manifest from .env.

| File | Audience | Must not contain | Takeaway |
| --- | --- | --- | --- |
| manifest.json / .webmanifest | Browsers installing the PWA | Secrets, admin start_url, tokens | Public by design |
| package.json | npm / curious GETs | Private app inventory if the app is private | See public package.json |
| assetlinks.json / AASA | OS App Links | Debug team IDs, secrets | See assetlinks vs AASA |
| security.txt | Researchers | VPN hostnames | Different well-known |

### What the roast can prove

A 404 on the manifest is not a finding. A 200 package.json can be. Confirm start_url yourself. Pair with tokens in URLs if start_url still carries a query secret, and with sitemap.xml if you listed /manifest.json next to /backup.zip.

### Common mistakes

The first mistake is start_url: '/dashboard' on a site that 302s unauthenticated users through a tokenized URL.

The second mistake is stuffing STRIPE_PK into the manifest 'for the installed app.'

The third mistake is treating a missing manifest like a missing CSP.

### FAQ

**Is a public web app manifest a vulnerability?**

No. It is supposed to be public. Secrets or an admin start_url in it are the miss.

**Should I robots.txt Disallow the manifest?**

Browsers still need it. Disallow is not access control and can break install.

**Does RoastMyOpsec fetch manifest.json?**

Not as a scored path. Missing it is not an OPSEC F.

---

## URL Scan vs GitHub Deepen for OPSEC Audits

- URL: https://roastmyopsec.com/blog/url-scan-vs-github-deepen
- Category: News
- Updated: 2026-04-16

URL scan vs GitHub deepen is a visibility split, not two competing products. A public URL audit sees what the internet already gets: headers, cookies, mixed content, robots.txt, and well-known path leaks. GitHub deepen, when you authorize it, reads repository contents you choose — committed secrets, reckless Actions permissions, lockfile risk — things HTTP cannot see. Start with a URL you own. Add GitHub only if that repo actually ships the site. RoastMyOpsec does not scan private networks or run exploit payloads in either mode.

### The practical difference

The website is the costume. The repo is the dressing room. A clean costume with .env in git history is still an incident. A locked-down repo with .git on the CDN is still an incident. You need the surface that matches the leak.

OAuth deepen is optional and scoped. A URL roast never requires connecting GitHub.

### How to choose what to run this week

If you shipped a marketing site this month, run website mode on that host. If the same team pushes to GitHub and you suspect history is messy, deepen the repo that builds that host — not a random org dump.

| Question | URL scan | GitHub deepen | Takeaway |
| --- | --- | --- | --- |
| Are headers and cookies adult? | Yes | No | HTTP is the source of truth |
| Is .env on the CDN? | Yes (path probes) | Maybe also in git | Fix the edge and rotate |
| Did someone commit a cloud key? | Only if it shipped in JS | Yes — history included | Rotate; purge if you still control history |
| Are Actions overly trusted? | No | Yes | Repo-only finding |

### When deepen is the wrong next click

Connecting a monorepo that is not the site you just roasted wastes time. Connecting a repo you do not own is against the product's consent model. If the leak is mixed content, GitHub will not rewrite the CMS.

If the URL already showed public JS secrets, rotate first, then deepen to see whether git still holds the old key.

### Common mistakes

The first mistake is treating a 9/10 URL score as a clean repo.

The second mistake is deepening a fork that never deploys.

The third mistake is leaving GitHub connected after the one-time check if you did not intend a standing grant.

### FAQ

**Does a URL scan replace a git secrets review?**

No. The URL cannot see commits that never shipped to the CDN. Deepen is how you inspect a repo you authorize.

**Is GitHub required for RoastMyOpsec?**

No. Optional. The free audit is the public URL. Deepen adds repo-level findings on the same report.

**Will deepen download my entire company?**

It reads repositories you authorize for the check types documented — secrets, Actions, lockfile risk — not a pentest of GitHub the company.

---

## Topics API vs Third-Party Cookies for Marketing Sites

- URL: https://roastmyopsec.com/blog/topics-api-vs-third-party-cookies
- Category: News
- Updated: 2026-03-22

Topics API vs third-party cookies for marketing sites is two advertising identity models. Third-party cookies (especially unpartitioned SameSite=None) let an embed recognize the same browser across sites. The Topics API (Privacy Sandbox) lets participating sites observe coarse interest topics the browser computed — not a durable cross-site cookie, still a measurement channel. Neither is access control. A brochure that needs pageviews can stay on first-party analytics and skip both. Permissions-Policy can restrict browsing-topics. RoastMyOpsec pattern-matches common analytics and marketing hosts in public HTML. It does not score Topics, does not call document.browsingTopics(), and does not treat missing Topics as an F.

### The practical difference

A third-party cookie is a shared jar. Partitioned cookies (CHIPS) isolate per top-level site. Topics is an API on the page origin after the user has a history the browser classifies. Client Hints are another browser-described surface. See partitioned cookies, Storage Access API, and Client Hints vs User-Agent.

Adding a Topics snippet because 'cookies are dying' still adds a third-party script unless you use a first-party wrapper you actually control.

### How to choose a measurement plan

Need counts on your own site: first-party analytics, tight cookies, no None on the session. Need ads: talk to counsel and vendors; do not paste unpartitioned trackers 'until Topics works.' Use Permissions-Policy to disable unused Topics if you do not participate. ads.txt is authorized sellers, not a tracker toggle.

| Channel | What it shares | Brochure default | Takeaway |
| --- | --- | --- | --- |
| Unpartitioned third-party cookie | Cross-site identity | Avoid | High CSRF and tracking cost |
| Topics API | Coarse interests, if enabled | Skip unless you sell ads | Not a roast F if missing |
| First-party analytics | Your origin's counts | Usual win | See trackers vs first-party |
| Permissions-Policy | Can restrict browsing-topics | Disable unused APIs | See Permissions-Policy guide |

### When Topics still wins

Topics wins as a vendor requirement on a site that already runs ads and has a privacy review. First-party analytics still wins for a brochure. Missing Topics is healthy. Pair with ads.txt if you actually sell inventory.

### What the roast can prove

Known tracker hosts in HTML can surface. Topics participation is not scored. Confirm Permissions-Policy and scripts yourself. Pair with consent-based scanning — do not roast third-party ad hosts as research.

### Common mistakes

The first mistake is keeping unpartitioned ad cookies 'as a backup' after adding Topics.

The second mistake is treating Topics as a security header.

The third mistake is requiring Topics as an OPSEC finding.

### FAQ

**Is missing Topics API a vulnerability?**

No. It is an ads/measurement choice. Missing it is not a roast F.

**Does Topics replace first-party analytics?**

No. Topics is an interest signal for participating ad flows. Pageviews still belong first-party if you need them.

**Does RoastMyOpsec call the Topics API?**

No. It may pattern-match known marketing hosts in HTML. It does not observe topics.

---

## TLS 1.2 vs TLS 1.3 for Public Marketing Sites

- URL: https://roastmyopsec.com/blog/tls-12-vs-tls-13-for-public-sites
- Category: News
- Updated: 2026-03-05

TLS 1.2 vs TLS 1.3 for public marketing sites is which handshake the edge offers, not whether the URL is HTTPS. TLS 1.3 is the current generation: fewer round trips, no renegotiation theater, modern AEAD-only ciphers. TLS 1.2 remains for older clients until you have data they are gone. TLS 1.0 and 1.1 belong off. A working cert and HSTS still matter more for OPSEC than winning a cipher beauty contest. RoastMyOpsec's free TLS check confirms the audited URL lands on HTTPS and that the runtime trust store accepts the certificate. It does not enumerate cipher suites, does not test expiry calendars, and does not score missing TLS 1.3 as an F.

### The practical difference

HTTPS as a scheme is 'use TLS.' The version is 'which TLS.' A site on TLS 1.2 only is still HTTPS. A site that still offers 1.0 is the miss. Mixed content and HSTS are orthogonal — see those guides.

CDN defaults often enable 1.2+1.3. Origin-only TLS behind the CDN can lag. Align both.

### How to choose a policy

Start from Mozilla Intermediate or Modern SSL config on the edge. Drop 1.0/1.1. Keep 1.2 until analytics or support prove otherwise. Turn on 1.3. Do not disable 1.2 the week before a government-client RFP. Certificate expiry automation first — HSTS plus an expired leaf is a lockout.

| Generation | Typical role in 2026 | Roast today | Takeaway |
| --- | --- | --- | --- |
| TLS 1.3 | Prefer on public edges | Not scored as required | Enable it |
| TLS 1.2 | Compatibility | Not a finding by itself | Keep until clients move |
| TLS 1.0 / 1.1 | Legacy | Not a cipher scan | Turn off at the CDN |
| Valid cert + HTTPS URL | This visit works | Free TLS check | The roast actually tests this |

### What the roast can prove

Scheme and trust-store acceptance on the submitted URL. Cipher lists, 1.3-only, and OCSP stapling are out of scope. Pair with certificate expiry vs HSTS and Alt-Svc if you also advertise HTTP/3.

### Common mistakes

The first mistake is TLS 1.3-only on a site whose payment iframe still needs 1.2.

The second mistake is hardening origin ciphers while the CDN still offers 1.0.

The third mistake is treating a roast TLS pass as a Qualys A+.

### FAQ

**Is TLS 1.2 a vulnerability?**

Not by itself in 2026. TLS 1.0/1.1 are the versions to remove. Prefer offering 1.3 alongside 1.2.

**Does RoastMyOpsec grade cipher suites?**

No. It checks HTTPS and the runtime trust store. It does not scan ciphers or expiry calendars.

**Should brochure sites disable TLS 1.2?**

Only with evidence. Missing TLS 1.3 is not a roast F.

---

## Third-Party Trackers vs First-Party Analytics

- URL: https://roastmyopsec.com/blog/third-party-trackers-vs-first-party-analytics
- Category: News
- Updated: 2026-02-07

Third-party trackers vs first-party analytics is a blast-radius choice, not a reporting feature checklist. A third-party pixel executes someone else's JavaScript in your users' browsers. First-party analytics collects events on a host you control, often with no extra script origins. Marketing sites that load a tag manager plus a pile of ad pixels accumulate XSS, supply-chain, and privacy debt with every friendly logo. RoastMyOpsec's vault tracker check pattern-matches major beacon hosts in the landing HTML. It does not click ads or profile your visitors.

### The practical difference

Third-party tracking is a remote program running beside your product. You inherit that vendor's outage, breach, and CSP exceptions. First-party collection keeps the script and the endpoint on origins you ship and monitor.

Server-side tagging can shrink the browser surface, but it is not magic: you still decide which vendors receive the data and which scripts remain on the page.

### How to choose based on what you actually need

If you need pageviews and a few conversion events, a first-party or privacy-respecting self-hosted tool is usually enough. If paid acquisition requires a specific conversion API, prefer a server-side connector over an unbounded client pixel. Do not install a tag manager 'in case marketing wants it later.'

| Need | Third-party pixels | First-party / server events | Takeaway |
| --- | --- | --- | --- |
| Basic traffic | Usually overkill | Better default | Skip the ad-tech zip code |
| Paid ads attribution | Often demanded by the network | Conversion APIs without extra JS | Negotiate the smallest client footprint |
| A/B tests and chat widgets | Each is another origin | Ship fewer experiments | CSP and SRI still apply |
| Tag manager | A loader for more loaders | Avoid unless you staff it | Unbounded script is the OPSEC miss |

### What still shows up on public sites

Brochure sites copy a 'standard' GTM container from a previous agency. The container still fires pixels for products they no longer run. Website-mode scans keep finding those beacons because the HTML does not forget.

Pair a shorter tag list with CSP script-src and a Permissions-Policy that does not grant extra device APIs to marketing scripts.

### Common mistakes

The first mistake is one tag manager plus every network 'just to test.'

The second mistake is putting analytics on the app origin that holds sessions.

The third mistake is blocking trackers in robots.txt and leaving the scripts in the page.

### FAQ

**Are third-party analytics always a vulnerability?**

Not automatically. They expand XSS and supply-chain risk and often conflict with a tight CSP. Treat each script as code you chose to run.

**Does first-party analytics mean cookies are safe?**

No. First-party collection can still use cookies. Grade cookie flags separately. The win is fewer foreign script origins.

**How does RoastMyOpsec detect trackers?**

It pattern-matches common analytics and marketing hosts in public HTML. Absence of a known beacon is not a privacy certification.

---

## Shared Storage API vs Topics API for Marketing Sites

- URL: https://roastmyopsec.com/blog/shared-storage-api-vs-topics-api
- Category: News
- Updated: 2025-11-14

Shared Storage API vs Topics API for marketing sites is two Privacy Sandbox ads channels. Topics lets participating sites observe coarse interest topics the browser computed. Shared Storage lets an origin write data that later runs in a worklet — often with a fenced frame — so select outputs can be used without a third-party cookie jar. Neither is access control. A brochure that needs pageviews can stay on first-party analytics and skip both. Permissions-Policy can restrict shared-storage and browsing-topics. RoastMyOpsec pattern-matches common analytics and marketing hosts in public HTML. It does not score Shared Storage, does not run worklets, and does not treat missing Shared Storage as an F.

### The practical difference

Topics is a coarse interest signal. Shared Storage is origin-keyed storage that is not freely readable by the page the way localStorage is — outputs are gated. Attribution Reporting measures conversions. Fenced frames are the opaque embed. Unpartitioned cookies are the old shared jar.

Pasting a Shared Storage snippet because 'cookies are dying' still adds a third-party script unless you control the worklet.

### How to choose a measurement plan

Need counts on your own site: first-party analytics. Need ads frequency or A/B in a Privacy Sandbox stack: Shared Storage after a vendor and privacy review. Need interest categories: Topics, same review. Use Permissions-Policy to disable unused shared-storage and Topics if you do not participate.

| Channel | What it holds | Brochure default | Takeaway |
| --- | --- | --- | --- |
| Topics API | Coarse interests | Skip unless you sell ads | See Topics vs cookies |
| Shared Storage | Worklet-gated origin data | Skip unless you run ads | Not a roast F if missing |
| First-party analytics | Your origin's counts | Usual win | See trackers vs first-party |
| Fenced frames | Opaque ads embed | Skip unless required | See fenced frames vs sandbox |

### When Shared Storage still wins

It wins as a vendor requirement on a site that already runs ads and has a privacy review. First-party analytics still wins for a brochure. Missing Shared Storage is healthy. Pair with fenced frames if the output is meant to stay opaque.

### What the roast can prove

Known tracker hosts in HTML can surface. Shared Storage participation is not scored. Confirm Permissions-Policy and scripts yourself. Pair with consent-based scanning — do not roast third-party ad hosts as research.

### Common mistakes

The first mistake is treating Shared Storage as a replacement for HttpOnly session cookies.

The second mistake is requiring window.sharedStorage as an OPSEC finding.

The third mistake is leaving shared-storage wide open in Permissions-Policy when you do not use it.

### FAQ

**Is missing Shared Storage a vulnerability?**

No. It is an ads measurement choice. Missing it is not a roast F.

**Does Shared Storage replace Topics?**

No. Topics is coarse interests. Shared Storage is worklet-gated origin data. Brochures usually want neither.

**Does RoastMyOpsec call Shared Storage?**

No. It may pattern-match known marketing hosts in HTML. It does not run worklets.

---

## Service Workers vs Cache-Control for Personalized Pages

- URL: https://roastmyopsec.com/blog/service-worker-vs-cache-control
- Category: News
- Updated: 2025-10-28

Service workers vs Cache-Control for personalized pages is two caches. Cache-Control tells browsers and shared CDNs whether HTTP may store the response. A service worker sits in the page origin and can cacheRequest even when the network said no-store — that is a product choice, not a header bug. Caching /dashboard HTML or API JSON with cookies in Cache Storage is how one profile's page survives logout. Scope the worker to a prefix you understand. RoastMyOpsec does not register service workers, does not inspect Cache Storage, and does not score missing Service-Worker-Allowed. It does compare Cache-Control to Set-Cookie on the landing HTML.

### The practical difference

HTTP cache is hop-by-hop policy. The worker is your JavaScript. Clearing cookies does not always clear Cache Storage. Clear-Site-Data on logout is the belt if you used a worker — see Clear-Site-Data vs cookie expiry.

A PWA for a public brochure can cache static assets. An account app should network-first for HTML and never put session bodies in Cache Storage without a threat model.

### How to choose a caching plan

Static hashed assets: worker or HTTP cache, long TTL. Personalized HTML: Cache-Control private, no-store, and skip the worker cache (or cache only a logged-out shell). Logout: unregister or skipWaiting plus Clear-Site-Data caches. Do not set Service-Worker-Allowed: / unless the worker is built for the whole origin.

| Layer | Who honors it | Personalized HTML | Takeaway |
| --- | --- | --- | --- |
| Cache-Control | Browser HTTP cache, CDNs | private, no-store | See ETag vs Cache-Control |
| Service worker Cache Storage | Your script | Usually do not cache | Scope + logout hygiene |
| ETag | Revalidation | Must not be user-agnostic on a public URL | Validator, not a worker |
| Clear-Site-Data | Supporting browsers on logout | Can wipe cache | Belt with cookie expiry |

### What the roast can prove

Set-Cookie plus public Cache-Control on the landing response can surface. A service-worker.js 200 is not scored. Confirm registration in your own app. Pair with the web app manifest if install is the reason you added a worker.

### Common mistakes

The first mistake is cache.addAll(['/']) including the logged-in document.

The second mistake is a worker at / with scope / on a host that also serves /admin.

The third mistake is treating Cache-Control as a kill switch for Cache Storage.

### FAQ

**Does Cache-Control no-store stop service workers?**

No. The worker can still write Cache Storage. Treat SW caching as a separate policy.

**Should brochure sites use a service worker?**

Only for static assets you are willing to cache. Missing a worker is not a roast F.

**Does RoastMyOpsec inspect service workers?**

No. It does not register workers or read Cache Storage.

---

## Server-Timing vs Server Banners for Stack Fingerprints

- URL: https://roastmyopsec.com/blog/server-timing-vs-server-banners
- Category: News
- Updated: 2025-10-20

Server-Timing vs Server banners for stack fingerprints is two ways the origin writes its résumé into HTTP. Server and X-Powered-By are explicit product strings. Server-Timing is a performance header (metric name, duration, optional description) meant for RUM and DevTools. On a public marketing response it often names db, graphql, or a route-shaped metric — a quieter inventory than phpinfo, still not a secret. Timing-Allow-Origin is a cousin that lets other origins read those timings. RoastMyOpsec inspects public landing headers for banners. It does not currently score Server-Timing, does not parse metric names into a map, and does not run timing attacks.

### The practical difference

A banner is a static boast. Server-Timing is a per-request trace you chose to publish. Client Hints are the other direction: the browser describing itself. CDN fingerprints (cf-ray, via) are yet another layer. Strip banners at the proxy. Keep Server-Timing off public HTML or use generic names with no descriptions.

Authenticated apps may keep detailed timings on a private origin. Do not copy that policy onto the brochure host. See Timing-Allow-Origin vs CORS if you also open timings to other sites.

### How to choose what to emit

Public marketing HTML: no Server-Timing, or a single opaque metric if a vendor requires it. APIs behind a dashboard: timings on that host only, still avoid SQL text in descriptions. Edge: strip Server-Timing from cacheable public responses even if the origin added it for debugging.

| Header | Typical leak | Public origin | Takeaway |
| --- | --- | --- | --- |
| Server / X-Powered-By | Product and version | Strip | See banners vs fingerprinting |
| Server-Timing named metrics | Backends, routes, durations | Omit or generic names | Not a scored roast F; still hygiene |
| Timing-Allow-Origin * | Other origins can read timings | No on personalized APIs | See TAO vs CORS |
| phpinfo / Actuator | Full runtime dump | No | Different costume, same class |

### When Server-Timing still wins

It wins on an internal observability host you authenticate to. It does not win as a substitute for stripping X-Powered-By. Missing Server-Timing on a brochure is healthy.

### What the roast can prove

Banner-shaped headers on the HTML response can surface. Server-Timing is not scored. Confirm in DevTools yourself. Pair with verbose API errors if the same request also dumps stacks.

### Common mistakes

The first mistake is leaving a debug Server-Timing middleware on the production CDN.

The second mistake is metric names that are the SQL table or GraphQL operation.

The third mistake is treating a roast miss on banners as proof no other fingerprint headers exist.

### FAQ

**Is Server-Timing a vulnerability?**

Detailed names and descriptions on a public origin are an information leak. A missing header is fine.

**Should brochure sites send Server-Timing?**

Usually no. Keep RUM on first-party analytics without publishing backend names.

**Does RoastMyOpsec score Server-Timing?**

No. It inspects banners on the landing response. It does not parse timing metrics or run timing attacks.

---

## Server Banners vs Stack Fingerprinting

- URL: https://roastmyopsec.com/blog/server-banners-vs-stack-fingerprinting
- Category: News
- Updated: 2025-10-03

Server banners vs stack fingerprinting is a disclosure choice, not a cloak of invisibility. A versioned Server or X-Powered-By header hands strangers a tech résumé with every response. Removing those banners at the reverse proxy is cheap hygiene. It does not hide TLS fingerprints, HTML generator tags, JS bundles, or error prose. RoastMyOpsec's vault fingerprint check is passive header inspection on the primary response — no probe payloads, no CVE exploitation.

### The practical difference

A banner is an explicit string you chose to send (or forgot to turn off). Fingerprinting is everything else: cipher suites, cookie names, default 404 pages, WordPress paths, Next.js build IDs.

Turning off X-Powered-By: Express is worth doing. Pretending the site is now unidentifiable is not. Attackers still have HTML and JS.

### How to choose what to strip

Strip Server and X-Powered-By at nginx, Cloudflare, Vercel headers, or the app framework. Keep generic error pages. Do not spend a sprint on banner games while .env is public.

| Signal | Fix | What remains | Takeaway |
| --- | --- | --- | --- |
| X-Powered-By / Server version | Remove at proxy | Other headers, HTML | Do it this afternoon |
| Generator meta tags | Remove from templates | Asset paths still talk | Cheap if you own the theme |
| Verbose 500 HTML | Generic client errors | Logs should stay detailed server-side | Banners are not the only résumé |
| Framework cookie names | Rename if you must | Behavior still fingerprints | Low priority vs real holes |

### What still shows up this week

Managed hosts inject Server: cloudflare or similar. That is inventory, not a CVE. The miss is advertising Express 4.17.1 or PHP/7.4.3 when those versions have a public patch story.

Pair banner stripping with keeping dependencies actually updated. A quiet header on an ancient runtime is cosplay.

### Common mistakes

The first mistake is treating banner removal as the security program.

The second mistake is stripping headers on / and leaving them on /api.

The third mistake is a custom Server: SecureSite/1.0 that still unique-fingerprints you.

### FAQ

**Is a Server header a vulnerability?**

Usually no. It is reconnaissance. Versioned banners that name unpatched software make that reconnaissance faster.

**Will hiding banners stop attacks?**

No. It removes one easy hint. Patch, headers, and exposure checks matter more.

**How does RoastMyOpsec read banners?**

Passive inspection of the primary response headers. No exploit payloads against named versions.

---

## security.txt vs a Public /security Contact Page

- URL: https://roastmyopsec.com/blog/security-txt-vs-public-security-contact
- Category: News
- Updated: 2025-09-24

security.txt vs a public /security contact page is a channel split, not a lock. RFC 9116 security.txt lives at https://example.com/.well-known/security.txt and tells researchers where to send a report (Contact, Expires, Preferred-Languages, optional Policy and Canonical). A /security or /responsible-disclosure HTML page is for humans and legal tone. You usually want both: the file for tools and the page for people. Neither authenticates anyone or hides /.env. RoastMyOpsec is a consent-based roast of URLs you assert you own. It does not require security.txt, does not file bugs for you, and does not scan third-party sites as 'research.'

### The practical difference

security.txt is a small, signed-optional text file at a well-known path. Parsers look there first. Expires is required so stale mailboxes do not become a sink. Contact should be a mailbox or URL you actually monitor — not a 404.

A public security page is narrative: scope, safe harbor language your counsel approved, and what you will not pay for. It will not be found by every scanner unless you also ship security.txt or link it from the footer.

### How to choose what to publish

If you have no intake mailbox, do not publish a Contact you will ignore. Stand up an alias, then add security.txt with a near-term Expires you will renew. If legal needs a policy URL, add Policy: and keep the HTML page in sync. Do not list a personal Gmail you will abandon.

If you run a bug bounty, point Policy or Contact at the program, not a generic info@ that auto-replies 'we do not accept unsolicited reports' while the file still invites them.

| Channel | Audience | What it is not | Takeaway |
| --- | --- | --- | --- |
| /.well-known/security.txt | Tools and researchers who know RFC 9116 | Access control; a pentest letter | Publish if you will actually read Contact |
| /security HTML | Humans, counsel, procurement | A substitute for the well-known path | Keep policy language here; link both ways |
| robots.txt Disallow | Crawlers | A disclosure inbox | See robots vs access control |
| RoastMyOpsec roast | You, on a URL you own | A researcher filing a CVE | Self-audit; still publish a real Contact |

### What this has to do with OPSEC

A missing security.txt is not a vulnerability. A Contact that bounces, or a file that advertises a dead Slack webhook, is an OPSEC miss: good-faith reports go to /dev/null while the same bugs stay public. Do not put secrets, VPN hostnames, or internal Jira in the file.

The roast may probe well-known sensitive paths for signatures. security.txt is a disclosure aid, not one of those leak classes. Presence is optional; honesty of Contact is not.

### Common mistakes

The first mistake is security.txt at the site root only, skipping /.well-known/security.txt.

The second mistake is an expired Expires and a Contact nobody owns.

The third mistake is using the file to 'invite' scans of hosts you do not control.

### FAQ

**Do I need security.txt to pass a RoastMyOpsec scan?**

No. The roast is a consented public-surface audit. security.txt is for humans and tools that want to report issues. It is not a scoring gate.

**Is a /security page enough without security.txt?**

It is enough for visitors who click it. Researchers who look up RFC 9116 will miss you. Ship both if you want reports.

**Should I list my production admin URL in security.txt?**

No. Publish a contact and a policy. Do not donate inventory. See admin discoverability vs access control.

---

## security.txt Expires vs Stale Contact for Public Sites

- URL: https://roastmyopsec.com/blog/security-txt-expires-vs-stale-contact
- Category: News
- Updated: 2025-09-15

security.txt Expires vs a stale Contact for public sites is a freshness decision versus an intake-mailbox decision: RFC 9116 requires an Expires timestamp so parsers know the file is still intended, while Contact is the URL or email researchers should use. A file with a future Expires and a mailbox nobody reads is worse than omitting security.txt. RoastMyOpsec does not require security.txt and does not score a missing or expired file as a standalone F.

### The practical difference

RFC 9116 security.txt lives at /.well-known/security.txt. Contact and Expires are the core fields. Policy, Acknowledgments, Canonical, and Encryption are optional. Expires is not a certificate; it is how you promise the file is still current. After that date, consumers should treat the file as stale.

Stale Contact is the human failure: intern@, a departed employee, a Google Group that auto-expires, or a ticket form that 404s. Researchers who follow a dead Contact waste time or go public. humans.txt is not an alternate parser location. A /security HTML page can still help humans; the machine file still needs a live Contact.

Publishing security.txt is a commitment to read it. If legal will not let you, do not ship a decorative file with a personal Gmail and a two-year Expires.

### How to choose based on what you shipped

If you will triaging reports: ship security.txt with a monitored alias, a realistic Expires you will renew, and Canonical if you mirror hosts. If you will not read reports: omit the file rather than lie. Do not put the only Contact in humans.txt or a blog RSS item.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Fresh Expires + live Contact | You will actually read reports | Forgetting to renew Expires | This is a working disclosure file |
| Expired security.txt | It does not — parsers should ignore it | Leaving Contact that still looks official | Renew or remove |
| No security.txt | You will not staff an inbox | Researchers guessing webmaster@ | Honest empty state |
| HTML /security page only | You want human prose plus a form | Assuming parsers will scrape the HTML | Useful extra, not RFC 9116 |

### When a public HTML contact page still wins

A human /security page still wins for policy prose and a form people can understand. The well-known file still wins for parsers. An expired file with a dead Contact wins nothing. Missing security.txt is not a roast F; a decorative stale file is a process miss.

### Common mistakes

The first mistake is shipping Contact to a mailbox nobody monitors.

The second mistake is letting Expires lapse while leaving the file up as if it were current.

The third mistake is treating a missing security.txt as a RoastMyOpsec F.

### What a URL roast can prove

RoastMyOpsec does not require security.txt and does not score Expires, Contact, JSON-LD, or Open Graph as standalone Fs. The free roast is headers, TLS, cookies, bounded paths (.git/HEAD, .env, package.json signatures), and js-secrets on a consented URL. If you publish RFC 9116, keep Expires honest and Contact staffed; confirm the file yourself.

### FAQ

**Is Expires required in security.txt?**

Yes. RFC 9116 requires Expires so consumers can tell whether the file is still intended to be current.

**Is a missing security.txt a vulnerability?**

No. It is a missed intake channel if you wanted one. It is not a roast F.

**Can humans.txt hold the security mailbox instead?**

You can mention people there, but parsers look at /.well-known/security.txt. Put Contact in the RFC 9116 file.

---

## Security Headers Checklist for Public Websites

- URL: https://roastmyopsec.com/blog/security-headers-checklist-for-public-websites
- Category: News
- Updated: 2025-09-07

A security headers checklist for public websites is a browser-guardrail list, not a pentest. Ship Content-Security-Policy (start report-only if needed), Strict-Transport-Security with a long max-age, framing controls via CSP frame-ancestors plus X-Frame-Options, Referrer-Policy, X-Content-Type-Options=nosniff, and a tight Permissions-Policy. RoastMyOpsec's free audit reads those signals on the primary document response. Headers will not fix an exposed .env, but missing headers still make XSS, clickjacking, MIME confusion, and HTTPS downgrades easier.

### The checklist

Apply these on HTML document responses. APIs may need a different subset. Confirm at the CDN so every marketing route inherits the policy.

- CSP: default deny with explicit script, style, image, connect, and frame-ancestors. Report-only until it stops breaking checkout widgets.
- HSTS: max-age you can live with; includeSubDomains only if every subdomain is ready.
- Framing: frame-ancestors 'none' (or 'self') and matching X-Frame-Options.
- Referrer-Policy: strict-origin-when-cross-origin is a sane default for most sites.
- X-Content-Type-Options: nosniff.
- Permissions-Policy: disable camera, microphone, geolocation, and payment unless the page needs them.

### What changed in how teams ship this

Tag managers, chat widgets, and A/B tools are why CSP still dies in committee. The current pattern that works: lock framing, HSTS, nosniff, and referrer this week. Put CSP in report-only, collect violations, then enforce. Do not wait for a perfect nonce story before denying iframes.

Permissions-Policy is the quiet win. Marketing sites almost never need device APIs. Turning them off is a one-line edge rule.

### Header vs exposure

Headers are guardrails. They are not a substitute for keeping .git, .env, and source maps off the origin. Run the path checks too.

| Control | Blocks or slows | Does not replace | Takeaway |
| --- | --- | --- | --- |
| CSP | Many XSS and unwanted script sources | Server-side auth bugs | Hard, high leverage |
| HSTS | Later HTTP first hops | Mixed content already in HTML | Pair with HTTPS redirects |
| Framing headers | Clickjacking | Open redirects | Cheap on brochure sites |
| nosniff | MIME confusion | Wrong Content-Type on APIs | Still ship correct types |

### How RoastMyOpsec grades headers

One consented GET. No header injection. Findings map to the free 'browser guardrails missing in transit' pack when CSP, HSTS, framing, referrer, nosniff, or Permissions-Policy are weak or absent.

Paste a URL you own and treat the roast as a punch list for the edge config.

### Common mistakes

The first mistake is setting headers only on / and not on /blog/*.

The second mistake is a CSP that uses default-src * because a vendor asked nicely.

The third mistake is copying API Access-Control headers onto the document and calling it CSP.

### FAQ

**What security headers does a public website need first?**

HSTS, framing controls, nosniff, Referrer-Policy, Permissions-Policy, then CSP — CSP last only because it is the easiest to break, not because it matters less.

**Can I set these in meta tags instead of HTTP headers?**

Some CSP and referrer features have meta equivalents, but HSTS and X-Frame-Options do not. Prefer HTTP headers at the edge so every response is covered.

**Will a header checklist get me a 10/10 OPSEC score?**

No. Score also depends on TLS, cookies, sensitive paths, and client-side secrets. Headers are the first adult supervision, not the whole roast.

---

## RSS vs Atom for Security Blogs on Public Sites

- URL: https://roastmyopsec.com/blog/rss-vs-atom-for-security-blogs
- Category: News
- Updated: 2025-08-12

RSS vs Atom for security blogs on public sites is a feed-format decision: Atom (RFC 4287) is the IETF XML format with clearer ids and dates, while RSS 2.0 remains the format most aggregators still auto-detect. Pick one public feed of posts you already published; do not put embargo write-ups, reporter emails, or staging URLs in <item> bodies. RoastMyOpsec does not score RSS or Atom as a standalone F, and a feed is not RFC 9116 security.txt.

### The practical difference

RSS 2.0 is a widely implemented snapshot format with <item>, <guid>, and <pubDate>. Atom uses <entry>, <id>, and <updated> and is specified as RFC 4287. Both are XML you advertise with rel="alternate" type="application/rss+xml" or application/atom+xml. Readers that ignore MIME still sniff the root element.

A security blog feed is a public firehose. Full-content feeds duplicate the HTML. Excerpt feeds still leak titles. That is the point of a blog. The miss is treating the feed as a private advisory list, putting unfixed issue titles in <title> before the patch window, or linking attachments that are not meant to be world-readable.

security.txt Contact is how researchers reach you. The blog feed is how subscribers read essays. Mixing them is how a personal mailbox ends up in every aggregator.

### How to choose based on what you shipped

Ship Atom if you want a single RFC to point at and clean ids. Ship RSS if your CMS default is RSS and your audience’s readers expect it. Offering both is fine if they list the same public slugs. Do not invent a third “private RSS” on the marketing origin and then Disallow it in robots.txt as if that were a lock.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Atom (RFC 4287) | You want a standard with stable entry ids | Some older widgets still look for RSS only | Solid default for a new feed |
| RSS 2.0 | The CMS and most readers already speak it | Ambiguous dates and guid practices | Fine if the items stay public posts |
| JSON Feed | You control the clients and want JSON | Fewer random aggregators | Still a public document |
| No feed | A tiny site with no series | Not a security hole | Optional; not a roast F |

### When RSS still wins

RSS still wins when the publishing stack emits it cleanly and your readers already subscribed. Atom still wins for new custom feeds. A missing feed is healthy. A feed of non-public advisories on a marketing host is the miss.

### Common mistakes

The first mistake is putting embargo vulnerability titles in the public feed before the fix is out.

The second mistake is using the feed as a substitute for security.txt Contact.

The third mistake is treating a missing RSS file as an OPSEC finding.

### What a URL roast can prove

RoastMyOpsec does not fetch or score RSS, Atom, JSON-LD, Open Graph, or hreflang as standalone Fs. The free roast is headers, TLS, cookies, path probes (.git/HEAD, .env, package.json signatures), and js-secrets. Keep feeds to public posts; put researcher intake in security.txt if you will actually read it.

### FAQ

**Is Atom more secure than RSS?**

No. Both are public XML. Security is what you put in the entries and whether those URLs should be public.

**Can a feed replace security.txt?**

No. RFC 9116 parsers look at /.well-known/security.txt. A blog feed is for readers, not intake.

**Does RoastMyOpsec require a feed?**

No. Missing RSS or Atom is not a scored F.

---

## Related Website Sets vs Partitioned Cookies for Brands

- URL: https://roastmyopsec.com/blog/related-website-sets-vs-partitioned-cookies
- Category: News
- Updated: 2025-07-18

Related Website Sets vs partitioned cookies for brands is two ways browsers treat cookies across related origins. Partitioned cookies (CHIPS) isolate a third-party cookie per top-level site so site A and site B do not share the embed's jar. Related Website Sets (RWS) lets a primary site declare a small set of related domains so participating browsers may treat some storage as same-party within that set — still not a security boundary. Neither replaces SameSite=Lax on your login cookie. A brochure that only needs its own origin can skip RWS entirely. RoastMyOpsec grades Set-Cookie on the landing response and pattern-matches known trackers. It does not fetch /.well-known/related-website-set.json, does not score RWS, and does not load you in third-party iframes.

### The practical difference

CHIPS is a cookie attribute: Partitioned. RWS is a declared list of sites, historically via a well-known JSON file and a submission process. Topics is an ads interest API. Storage Access API is a user-gesture path for embeds. Do not stack all four because a vendor slide said 'cookies are changing.'

Declaring shop.example and news.example as related does not make a shared session cookie safe. CSRF and XSS still follow the cookie flags and the origin that set them.

### How to choose for a public brand

One marketing origin: first-party analytics, Lax sessions, no RWS. Widget embed that must remember a user on other sites: Partitioned None on a dedicated cookie, not the login cookie. Multiple first-party hostnames that already share a login: keep the session first-party per host or use a documented SSO — do not treat RWS as SSO. Permissions-Policy still governs unused device APIs.

| Control | What it groups | Brochure default | Takeaway |
| --- | --- | --- | --- |
| Partitioned (CHIPS) | Embed cookie per top-level site | Use if you must embed | See partitioned vs None |
| Related Website Sets | Declared related domains | Skip unless you operate a set | Not a roast F if missing |
| First-party Lax session | Your login origin only | Usual win | Do not mark it None |
| Topics API | Coarse ad interests | Skip unless you sell ads | See Topics vs cookies |

### When Related Website Sets still wins

RWS wins when you already operate a small, documented set of first-party domains and a privacy review blessed the declaration. Partitioned cookies still win for third-party widgets. Missing RWS is healthy. Pair with third-party trackers vs first-party analytics if the 'set' is actually an ad graph.

### What the roast can prove

Set-Cookie flags and known tracker hosts in HTML can surface. Related Website Sets membership is not scored. Confirm the well-known file and cookie attributes yourself. Pair with consent-based scanning — do not treat another brand's set as research.

### Common mistakes

The first mistake is treating RWS as a WAF or as proof third-party cookies are gone.

The second mistake is putting the login cookie in a set so 'users stay signed in everywhere.'

The third mistake is requiring a related-website-set.json as an OPSEC finding.

### FAQ

**Is missing Related Website Sets a vulnerability?**

No. It is a Privacy Sandbox grouping choice. Missing it is not a roast F.

**Does RWS replace partitioned cookies?**

No. CHIPS isolates embed cookies per top-level site. RWS declares related first-party domains. Login cookies should stay first-party Lax.

**Does RoastMyOpsec fetch related-website-set.json?**

No. It reads landing Set-Cookie and may pattern-match known marketing hosts. Confirm the well-known file yourself.

---

## rel=me vs sameAs for Brand Identity on Public Sites

- URL: https://roastmyopsec.com/blog/rel-me-vs-sameas-for-brand-identity
- Category: News
- Updated: 2025-07-09

rel=me vs sameAs for brand identity on public sites is a handshake-versus-claim decision: rel=me (microformats) is a public link meant to be reciprocal for IndieWeb-style verification, while schema.org sameAs is a JSON-LD statement that this Organization or Person is also at those URLs. Use sameAs for search graphs; use rel=me when you want bidirectional proof. RoastMyOpsec does not score rel=me or sameAs as standalone Fs, and neither is a password.

### The practical difference

rel=me on an <a> or <link> says “this other profile is me.” Verifiers typically require the remote profile to link back. That is still public HTML, not OAuth. sameAs on Organization is a list of profile URLs in structured data; Google may use it as a hint for a knowledge panel. There is no protocol requirement that the remote site agree.

The OPSEC miss is linking a personal staff Mastodon, a private GitHub org, or a staging dashboard as if it were the brand. humans.txt that repeats the same personal emails is the same class of overshare. Ads.txt and security.txt remain different files with different jobs.

Verified badges on social platforms are their own products. rel=me will not mint a blue check. sameAs will not either.

### How to choose based on what you shipped

Brand homepage: Organization sameAs to public profiles the company controls. Footer or about page: rel=me only where you will keep the reverse link. Do not rel=me a personal account as the company. Do not sameAs URLs you do not own.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| rel=me | You want IndieWeb-style bidirectional identity | One-way links and personal accounts on a brand origin | Handshake, still public |
| schema.org sameAs | You want a brand graph for search | Listing profiles you do not control | A claim, not a proof |
| humans.txt credits | Optional culture file with no PII | Personal emails and home addresses | Not an identity protocol |
| security.txt | Researcher intake | Putting Contact only in sameAs | Different well-known job |

### When rel=me still wins

rel=me still wins when you maintain reciprocal links for IndieWeb verification. sameAs still wins for Organization JSON-LD. Missing rel=me is healthy on a brochure site. Treating either as a secret identity vault is the mistake.

### Common mistakes

The first mistake is sameAs-listing social URLs the brand does not control.

The second mistake is rel=me to a personal staff account from the company origin.

The third mistake is treating missing rel=me as a security finding.

### What a URL roast can prove

RoastMyOpsec does not score rel=me, sameAs, JSON-LD, Open Graph, or humans.txt as standalone Fs. The free roast is headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets. Identity links are public branding; keep personal accounts and unpublished hosts out of them.

### FAQ

**Does sameAs verify that I own the profile?**

No. It is a public claim. rel=me verification looks for a reverse link, which is still not a secret.

**Should the company rel=me employee accounts?**

Usually no. Brand identity is brand profiles. Staff personal accounts are PII-adjacent on a marketing origin.

**Does RoastMyOpsec require rel=me?**

No. Missing rel=me or sameAs is not an F.

---

## rel=canonical vs 301 for Duplicates on Public Sites

- URL: https://roastmyopsec.com/blog/rel-canonical-vs-301-for-duplicates
- Category: News
- Updated: 2025-07-01

rel=canonical vs a 301 for duplicates on public sites is a stay-versus-move decision: a 301 tells clients and crawlers the old URL is gone, while rel=canonical is a hint that two live URLs represent the same document. Redirect www and apex (and http to https) with 301s; use canonical when print views, tracking parameters, or CMS aliases must remain reachable. RoastMyOpsec does not score canonical tags as a standalone F; it does care that HTTPS actually happens.

### The practical difference

A 301 is an HTTP redirect. Browsers follow it. Bookmarks update over time. Crawlers treat the target as the successor. rel=canonical is HTML (or an HTTP header) that says “this other URL is preferred” while the current URL still 200s. Google may ignore a canonical that contradicts redirects, hreflang, or internal links.

Host duplication is not a canonical problem first. Pick one hostname, redirect the rest, and put HSTS on the winner. Parameter duplication is often a canonical or parameter-handling problem. Content syndication is a canonical-plus-legal problem. Mixing them is how teams 301 the page users still need and leave three live hosts with three canonicals.

Canonical is not access control. A 200 with a canonical pointing at production from a staging host still serves the body to anyone who asks.

### How to choose based on what you shipped

If users and crawlers should never stay on URL A, 301 it to B and fix internal links. If A must remain a working document (print, share, filter) but B is the indexable one, canonical A to B and keep A out of the sitemap. If A and B are different languages, that is hreflang, not a fake canonical.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| 301 redirect | The old URL should stop existing as a destination | Chains and mixed www/apex/http stacks | Move traffic, then delete the duplicate |
| rel=canonical | Both URLs must 200 but one should rank | Hint can be ignored if the site contradicts it | Live duplicate with a preferred URL |
| Self-canonical | The page is the preferred URL | Pointing at a 404 or the wrong host | Useful hygiene, not a lock |
| hreflang | You actually ship language or region variants | Using canonical to smash locales together | Different job than duplicates |

### When rel=canonical still wins

Canonical still wins for print CSS views, tagged URLs you cannot redirect, and CMS aliases that would break inbound links if 301’d. A 301 still wins for protocol and hostname consolidation. Missing canonical on a unique article is not a roast F.

### Common mistakes

The first mistake is using rel=canonical instead of a 301 when www and apex should be one host.

The second mistake is canonicalizing staging to production while leaving staging world-readable.

The third mistake is treating a canonical tag as proof a duplicate is gone.

### What a URL roast can prove

The free roast checks whether the audited URL is served over HTTPS (and notes HTTP-to-HTTPS redirects), plus security headers, cookies, path probes, and js-secrets. It does not score rel=canonical, Open Graph, JSON-LD, RSS, or hreflang as standalone Fs. Pair host hygiene with the www-versus-apex guide; use the roast for leaks, not SEO theater.

### FAQ

**Should www to apex be a canonical or a 301?**

A 301 (and matching HSTS). Canonical is a hint on a live URL. Hostname consolidation is a redirect.

**Does canonical hide a page from attackers?**

No. The URL still responds. Redirect or deny if it should not be public.

**Does RoastMyOpsec grade canonical tags?**

No. Canonical is not a scored F. TLS on the URL you paste still is in scope for the free TLS check.

---

## Rate Limits vs CAPTCHA on Public Login Pages

- URL: https://roastmyopsec.com/blog/rate-limits-vs-captcha-on-public-login
- Category: News
- Updated: 2025-06-05

Rate limits vs CAPTCHA on public login is a layered abuse decision, not a single widget. Rate limits (and WAF bot rules) cap how often /login, /oauth/token, and password-reset can be hit from a network. CAPTCHA and similar challenges raise the cost of each automated attempt. Neither replaces MFA, SSO, or lockout policy on the account. Marketing /wp-login.php and app /oauth both need this; HTML-only limits leave the token endpoint lonely. RoastMyOpsec does not spray passwords or measure your limits. Presence of an auth path plus a missing edge fingerprint is a reminder to turn abuse controls on — not a proof they are off.

### The practical difference

A rate limit is a quota: too many tries from an IP, ASN, or credential and the edge or app answers slowly or with 429. It is cheap for humans and expensive for stuffing farms if the key is right (per account, not only per IP).

A CAPTCHA or managed bot challenge is a puzzle: the client must look like a person or a trusted token. It helps when attackers already have many IPs. It fails when the real API client cannot solve a widget, or when you only put the puzzle on /login HTML and not on /api/auth.

### How to choose based on the surface

Brochure WordPress: edge rate limit plus a challenge on wp-login.php, MFA on admin users, and no XML-RPC password guessing. Product apps: rate-limit token and reset routes at the WAF and in the app, challenge anonymous HTML login, keep machine clients on API keys or mTLS instead of a public puzzle.

If only the marketing host has a WAF and api. is the origin IP, stuffing walks around the costume. Pair this with the CDN vs origin and app-auth-path guides.

| Control | What it slows | What it does not do | Takeaway |
| --- | --- | --- | --- |
| IP / path rate limit | Volume from one network | Distributed stuffing; stolen valid passwords | Default on every auth route |
| Per-account lockout / backoff | Guessing one mailbox | Credential stuffing many accounts slowly | Needed with IP limits, not instead |
| CAPTCHA / bot fight | Cheap headless scripts on HTML login | Native apps and token APIs unless you add a server check | Put it where the form is; still rate-limit the API |
| MFA / SSO | Reuse of a stolen password | Session theft after login; XSS | Not optional for admin and customer accounts |

### What a URL roast can and cannot see

The scanner may notice common auth paths and CDN/WAF header fingerprints. It will not fire thousands of logins to prove a 429. A clean roast is not a load-test certificate. Confirm quotas in the WAF console and application logs.

Do not ask a public scanner to 'verify rate limits work.' That is an attack against yourself with extra steps, and it is out of scope here.

### Common mistakes

The first mistake is CAPTCHA on /login and unlimited POST to /oauth/token.

The second mistake is rate-limiting by IP only behind a carrier-grade NAT or a shared office.

The third mistake is treating a WAF fingerprint on www as proof that api. is protected.

### FAQ

**Is CAPTCHA better than rate limiting?**

You want both classes of control. Limits cap volume. Challenges raise cost. MFA still stops a stuffed password that gets through.

**Will RoastMyOpsec tell me if my rate limit is too high?**

No. It does not send credential-stuffing traffic. Check WAF and app metrics. The roast only inventories public auth doors and edge fingerprints.

**Should I hide /login so bots cannot find it?**

Hiding is not authentication. See admin discoverability vs access control. Rate-limit and MFA the real login wherever it lives.

---

## Private State Tokens vs CAPTCHA for Public Login

- URL: https://roastmyopsec.com/blog/private-state-tokens-vs-captcha
- Category: News
- Updated: 2025-04-15

Private State Tokens vs CAPTCHA for public login is two abuse-control costumes. CAPTCHA (and similar challenges) raises the cost of each attempt in the browser. Private State Tokens (formerly Trust Tokens) let a participating issuer mint a limited, non-tracking token that a redeemer can later check — a Privacy Sandbox anti-fraud signal, not a puzzle. Neither replaces rate limits, MFA, or access control on /wp-admin. A brochure with no login can skip both. RoastMyOpsec does not load-test logins, does not redeem tokens, and does not score missing Private State Tokens as an F. It may still inventory public auth doors and cookie flags on the URL you paste.

### The practical difference

CAPTCHA is a challenge you put on a form. Rate limits cap volume at the edge or app. Private State Tokens are a browser API between an issuer and a redeemer you actually operate — not a header you paste from a blog. WebAuthn cuts stuffing by replacing the password. FedCM is federated login, not bot scoring.

Shipping a token snippet because 'CAPTCHA is dying' still adds a third-party script unless the issuer is a vendor you already trust and reviewed.

### How to choose abuse controls

No login: skip tokens and skip CAPTCHA. Public login you own: rate-limit first, MFA on the account, CAPTCHA only if stuffing still hurts. Ads/fraud vendor requirement: Private State Tokens after a privacy review — they are not a roast finding if missing. Permissions-Policy can restrict private-state-token-issuance and redemption if you do not participate.

| Control | What it costs the attacker | Brochure default | Takeaway |
| --- | --- | --- | --- |
| Rate limits | Volume per IP/account | Usual first move | See rate limits vs CAPTCHA |
| CAPTCHA / challenge | Human or solver cost | If stuffing persists | Not a WAF by itself |
| Private State Tokens | Issuer/redeemer signal | Skip unless you participate | Not a roast F if missing |
| WebAuthn | Stolen passwords fail | If you have a login | See WebAuthn vs passwords |

### When Private State Tokens still win

They win when an ads or anti-fraud vendor you already use requires redemption and counsel signed off. Rate limits still win for a small login. Missing tokens is healthy. Pair with third-party trackers if the issuer is another origin's JavaScript.

### What the roast can prove

Public login paths and edge fingerprints can surface. Private State Token participation is not scored. Confirm Permissions-Policy and scripts yourself. Pair with consent-based scanning — do not roast someone else's issuer as research.

### Common mistakes

The first mistake is treating Private State Tokens as a CAPTCHA replacement and dropping rate limits.

The second mistake is requiring tokens as an OPSEC finding on a brochure.

The third mistake is leaving issuance and redemption wide open in Permissions-Policy when you do not use them.

### FAQ

**Is missing Private State Tokens a vulnerability?**

No. It is an anti-fraud participation choice. Missing it is not a roast F.

**Do Private State Tokens replace CAPTCHA?**

No. They are a limited signal between issuer and redeemer. Rate limits and MFA still belong on login.

**Does RoastMyOpsec redeem Private State Tokens?**

No. It does not load-test logins or call the token APIs. It may still inventory public auth doors.

---

## preload vs modulepreload for Public Pages

- URL: https://roastmyopsec.com/blog/preload-vs-modulepreload-for-public-pages
- Category: News
- Updated: 2025-03-29

preload vs modulepreload for public pages is a resource-hint decision: rel=preload fetches a specific asset early with an as= type, while rel=modulepreload is for ES modules and can fetch the module graph. Use preload for the LCP image or a critical font you will use immediately; use modulepreload for the first JS module the page will import. RoastMyOpsec does not score these hints as standalone Fs; over-preloading third-party URLs is a performance and tracker problem, not a headers F.

### The practical difference

MDN documents preload as a declarative fetch for something you will use on this navigation, with as="style"|"script"|"font"|"image" and matching CORS for fonts. modulepreload is specifically for classic ES module documents; browsers can discover static imports. Mixing them — preload as=script on a module, or modulepreload on a PNG — wastes connections.

Hints are not CSP. A preload to a third-party analytics host still starts a connection and may send cookies depending on attributes. Speculative loading of origins you do not need is how marketing pages pay for ads before the headline paints.

Early Hints (103) can carry the same Link relations. That is a CDN feature. It does not change the security model. CSP still has to allow what you preload.

### How to choose based on what you shipped

Preload one LCP image or one critical font with correct crossorigin. Modulepreload the entry ES module on app-like public pages. Do not preload every vendor chunk “just in case.” Do not modulepreload a URL you would not allow in script-src.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| rel=preload | One critical font, hero image, or blocking CSS | Wrong as= or unused preload warnings | Single asset, this navigation |
| rel=modulepreload | The page’s ES module entry and its static graph | Using it on classic scripts or images | Modules only |
| dns-prefetch / preconnect | You will definitely talk to that origin soon | Preconnecting ad and tracker origins | Connection setup, not a download |
| No hints | A simple document with few assets | Not a security failure | Healthy default |

### When preload still wins

preload still wins for a hero image or a font that is otherwise discovered late. modulepreload still wins for module-first pages. Neither wins as a way to sneak around CSP. Missing both is normal on a light marketing page.

### Common mistakes

The first mistake is preloading a third-party tracker origin and calling it performance work.

The second mistake is using modulepreload on non-module assets.

The third mistake is treating missing preload as a security finding.

### What a URL roast can prove

RoastMyOpsec’s free roast grades CSP and other security headers, TLS, cookies, path probes, and js-secrets. It does not score preload, modulepreload, fetchpriority, Open Graph, or JSON-LD as standalone Fs. If a preload URL is a tracker-shaped host in HTML, tracker findings may still surface — as trackers, not as a missing-hint F.

### FAQ

**Is modulepreload a security feature?**

No. It is a loading hint for ES modules. CSP still governs what may execute.

**Should I preload all fonts?**

No. Preload the one critical face you will use immediately, with correct crossorigin.

**Does RoastMyOpsec fail missing preload?**

No. Resource hints are not standalone Fs on the free roast.

---

## Pagination rel=next vs View-All for Public Sites

- URL: https://roastmyopsec.com/blog/pagination-rel-next-vs-view-all
- Category: News
- Updated: 2025-01-12

Pagination rel=next vs a view-all page for public sites is a series-architecture decision: Google no longer treats rel=next/prev as a ranking signal, so crawlable page-N URLs or a single view-all document have to stand on their own with sensible canonicals. Pick view-all when the full list is a reasonable size; pick numbered pages when the set is huge. RoastMyOpsec does not score rel=next as a standalone F, and pagination is not a way to hide URLs.

### The practical difference

rel=next and rel=prev in HTML or HTTP Link headers were a pagination hint. Google announced it does not use them for indexing. Other tools may still read them. They never were access control. A view-all URL is just another public document that may be heavy and may duplicate page-N content.

Canonical discipline matters more than the old rels. If page 2 should rank as itself, self-canonical. If view-all is the preferred URL, canonical page-N to it only when that matches what you want indexed — and when view-all actually contains the same items. Infinite scroll that never exposes crawlable links is how content disappears from search, not how it stays secret.

Do not paginate “private” records on a public origin and rely on page=47 being obscure. Obscure query strings are not a lock.

### How to choose based on what you shipped

Small catalogs: one view-all or a short page-1 that lists everything with ordinary links. Large catalogs: stable page-N URLs, unique titles, self-canonicals, and a sitemap of pages you want crawled. Keep rel=next if it helps humans and old clients; do not build OPSEC or ranking strategy on it.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| rel=next / rel=prev | You still want a hint for non-Google clients | Believing Google still uses it for ranking | Optional leftover, not a strategy |
| View-all page | The full set is a sane HTML size | Huge DOM, duplicate titles vs page-N | One preferred URL when it fits |
| Numbered crawlable pages | The set is too large for one document | Canonicalizing every page to page 1 blindly | Each page needs a real URL |
| Sitemap of series URLs | You want crawlers to find the pages | Listing URLs that should not be public | Advertise only what should be found |

### When a view-all page still wins

View-all still wins for a short changelog, a small team roster, or a finite FAQ index. Numbered pages still win for large catalogs. rel=next still wins only as a courtesy hint. Missing rel=next is not a roast F.

### Common mistakes

The first mistake is relying on rel=next/prev as a Google ranking feature after it was dropped.

The second mistake is canonicalizing every page-N to page 1 when the items are not on page 1.

The third mistake is treating obscure pagination as access control.

### What a URL roast can prove

RoastMyOpsec does not score rel=next, view-all architecture, JSON-LD, or hreflang as standalone Fs. The free roast is headers, TLS, cookies, path probes (.git/HEAD, .env, package.json signatures), and js-secrets. Pagination is information architecture; do not hide records behind page numbers and call it OPSEC.

### FAQ

**Does Google still use rel=next and rel=prev?**

Google said it does not use them as an indexing signal. Crawlable links and clear canonicals matter more.

**Is a view-all page more secure?**

No. It is another public URL. Security is whether those records should be public at all.

**Does RoastMyOpsec fail missing rel=next?**

No. Pagination hints are not a scored F.

---

## OSV Lockfile Advisories vs a Public URL OPSEC Scan

- URL: https://roastmyopsec.com/blog/osv-lockfile-advisories-vs-url-scan
- Category: News
- Updated: 2025-01-03

OSV lockfile advisories vs a public URL OPSEC scan are two inventories. A URL roast sees what browsers already get: headers, cookies, mixed content, path leaks, and secret-shaped strings in public JavaScript. An OSV match on package-lock.json says a pinned npm version appears in a known advisory database. That is a triage queue, not a proof the bug is reachable on your origin. RoastMyOpsec's optional GitHub deepen reads package-lock.json on a repo you authorize (up to a bounded set of packages) and queries OSV. It does not run exploits, does not parse every lockfile format, and a URL-only scan cannot see node_modules at all.

### The practical difference

The costume is HTTP. The dressing room is the lockfile that built the costume. A clean CSP with a lockfile full of unpatched lodash advisories is still homework. A patched lockfile with .env on the CDN is still an incident. You fix the surface that leaked.

OSV IDs are labels. Reachability, authz, and whether the vulnerable function ships to production are engineering work after the roast.

### How to choose what to run

Shipped a marketing site this week: URL mode first. Same team builds from GitHub with npm: deepen that repo, not a random package. If the lockfile is Yarn or pnpm, the current deepen may be inconclusive — enable Dependabot or equivalent for that format and still roast the URL.

| Question | URL roast | OSV on lockfile | Takeaway |
| --- | --- | --- | --- |
| Missing HSTS / CSP | Yes | No | HTTP is the source of truth |
| Known CVE in a pinned npm version | No | Yes, if package-lock.json is readable | Triage; do not skip headers |
| Secret in public JS | Yes | Only if it is also a dependency name | Rotate; see JS-secrets guide |
| Exploitability in prod | Not claimed | Not claimed | Neither tool is a pentest |

### What deepen actually does

It reads default-branch package-lock.json through the GitHub API, samples up to 100 package/version pairs, and asks OSV whether those versions have known vulns. History, yarn.lock, and whether webpack tree-shook the function are out of scope. Enable push protection and dependency alerts in GitHub regardless of the roast.

### Common mistakes

The first mistake is ignoring OSV hits because the URL score was 9/10.

The second mistake is treating every advisory as a production incident without reachability.

The third mistake is deepening a demo repo that is not what deploys roastmyopsec's target URL.

### FAQ

**Does a URL scan replace npm audit?**

No. The public site cannot see your lockfile. Use GitHub deepen or your own CI advisory tools on a repo you own.

**If OSV matches, is the site hacked?**

No. It means a known advisory exists for a locked version. Patch, then decide reachability. The roast does not exploit it.

**Why was the dependency check inconclusive?**

No package-lock.json, or a lockfile format deepen does not parse yet. Use Dependabot/Renovate for that ecosystem and still run the URL audit.

---

## Organization JSON-LD vs WebSite JSON-LD for Public Sites

- URL: https://roastmyopsec.com/blog/organization-jsonld-vs-website-jsonld
- Category: News
- Updated: 2024-12-09

Organization JSON-LD vs WebSite JSON-LD for public sites is an entity-versus-origin decision: Organization describes the brand (name, logo, sameAs profiles), while WebSite describes this site (url, name, optional SearchAction). Put both on the homepage when they stay true to visible content. RoastMyOpsec does not score JSON-LD types as standalone Fs, and sameAs is a public claim, not a login.

### The practical difference

Schema.org Organization is the legal or brand entity. Google’s organization structured data is how a knowledge panel may pick a logo. WebSite is the site as a creative work: its URL and sitename. SearchAction is the optional sitelinks search box pattern and must match a real public search URL.

Stuffing a personal cell number, a non-public office map, or an internal wiki into Organization is the OPSEC miss. sameAs should list profiles you actually control. WebSite.url should be the canonical public origin, not a staging host.

These types do not replace security.txt, ads.txt, or humans.txt. They are a graph for search and unfurlers.

### How to choose based on what you shipped

Homepage: Organization + WebSite in one JSON-LD @graph or two script tags, matching the visible brand. Inner articles: Article (or nothing) plus breadcrumbs if the trail is visible. Skip SearchAction if you do not have a public on-site search that returns that URL pattern.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Organization | You want a brand entity and logo in the graph | Private phones, internal addresses, fake sameAs | The company, not the URL |
| WebSite | You want sitename and the origin documented | Pointing url at staging | This host as a work |
| SearchAction | Public on-site search really exists at that URL | Advertising a search that 404s or requires auth | Only if the endpoint is public |
| Neither | A tiny page with no brand graph to maintain | Not a security hole | Optional for OPSEC |

### When WebSite JSON-LD still wins

WebSite still wins for sitename and a clean origin URL even when you skip a rich Organization. Organization still wins when you have a real logo and public profiles. Missing both is not a roast F. Conflicting names across JSON-LD, OG, and the visible header is a brand miss.

### Common mistakes

The first mistake is putting a SearchAction URL that is not a public search.

The second mistake is listing sameAs profiles the brand does not control.

The third mistake is treating missing Organization JSON-LD as a security finding.

### What a URL roast can prove

JSON-LD Organization, WebSite, breadcrumbs, Open Graph, and rel=me are not standalone Fs on RoastMyOpsec. The free roast covers security headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets. Keep the graph to public facts; confirm rich results in Search Console if you care about panels.

### FAQ

**Do I need both Organization and WebSite?**

They answer different questions. Many homepages ship both. Neither is required for a clean roast.

**Is sameAs a verified identity proof?**

No. It is a claim in public JSON-LD. Bidirectional rel=me is a different, still-public, handshake.

**Does RoastMyOpsec parse JSON-LD?**

Not as a scored check. Missing or messy structured data is not a free-pack F.

---

## OpenID Discovery vs security.txt on Public Origins

- URL: https://roastmyopsec.com/blog/openid-configuration-vs-security-txt
- Category: News
- Updated: 2024-11-30

OpenID Discovery vs security.txt on public origins is login metadata versus researcher contact. https://issuer.example/.well-known/openid-configuration (OpenID Connect Discovery) lists authorization, token, and JWKS URLs so clients can configure themselves. It is supposed to be public for that issuer. security.txt is a different well-known file for vulnerability disclosure. Mixing them is how teams hide OIDC behind a login and break every app, or paste an internal Keycloak URL into production discovery. Do not put client secrets in the document. JWKS is public keys, not private keys. RoastMyOpsec does not score openid-configuration or require security.txt. Missing discovery on a marketing host that is not an IdP is not an F.

### The practical difference

If this hostname is not an authorization server, you do not need openid-configuration. A brochure that proxies /oauth to a hidden IdP should not publish a discovery document that names the internal host. security.txt still belongs on the brand origin if you want reports.

assetlinks.json and apple-app-site-association are app-link files. The web app manifest is PWA metadata. All are public catalogs with different jobs.

### How to choose what to publish

IdP production origin: HTTPS discovery + JWKS, no secrets, issuer matching the cert name. Marketing origin: skip OIDC files unless you really issue tokens there. Staging issuers stay on staging hostnames. Redirect URIs in clients must not be open redirects — see tokens in URLs.

| File | Audience | Must not contain | Takeaway |
| --- | --- | --- | --- |
| /.well-known/openid-configuration | OIDC clients | Secrets, internal issuer hosts | Public by design on an IdP |
| JWKS URI from discovery | Token verifiers | Private signing keys | Public keys only |
| /.well-known/security.txt | Researchers | VPN hostnames | Different well-known |
| assetlinks / AASA | OS App Links | Debug team IDs | See assetlinks vs AASA |

### What the roast can prove

A 404 on discovery is not a finding on a marketing site. A 200 that is HTML from a SPA catch-all is a product bug for OIDC clients. The roast does not parse JWKS. Pair with dangling CNAMEs if an old issuer name still resolves.

### Common mistakes

The first mistake is discovery issuer: http://localhost:8080 copied from Keycloak.

The second mistake is putting client_secret in the JSON because 'it's well-known.'

The third mistake is treating missing openid-configuration like a missing CSP on roastmyopsec.com itself.

### FAQ

**Is public OpenID discovery a vulnerability?**

No, for an IdP. Internal hostnames or secrets in the document are the miss.

**Do I need discovery on a marketing site?**

Only if that host is the issuer. Otherwise skip it. Missing it is not a roast F.

**Does RoastMyOpsec fetch openid-configuration?**

Not as a scored path. security.txt is also not required to pass.

---

## Open Graph vs Twitter Cards for Public Sites

- URL: https://roastmyopsec.com/blog/open-graph-vs-twitter-cards
- Category: News
- Updated: 2024-11-13

Open Graph vs Twitter Cards for public sites is a preview-vocabulary decision: Open Graph (og:title, og:description, og:image, og:url) is the common unfurl format, while Twitter/X Cards add twitter:card and related names that many platforms now ignore in favor of OG. Ship accurate OG on public articles. Neither set is a security header, and RoastMyOpsec does not score missing OG or twitter: tags as a standalone F.

### The practical difference

Facebook’s Open Graph protocol defined the og:* meta properties most link unfurlers still read. Twitter Cards were a parallel set; X’s developer docs still describe summary and summary_large_image cards, but a complete OG block covers most modern unfurls. Duplicate titles that disagree with the visible H1 are a brand miss, not a CVE.

The OPSEC angle is oversharing in the preview: draft titles, internal product names, author personal emails, or images that include badges and whiteboard photos. og:image is a public file. Do not point it at an authenticated CDN path that only works with a cookie.

Trackers in the same <head> are a different problem. OG tags do not load a pixel by themselves. Third-party share buttons do.

### How to choose based on what you shipped

Put OG on every indexable article and the homepage. Add twitter:card only if you still see X unfurls that ignore OG. Keep images on the same public origin, HTTPS, and boring. Skip tags on pages you noindex if you do not want them unfurled from a pasted URL — or accept that the HTML is still public.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Open Graph | You want a default unfurl across messengers and social | og:image leaking drafts or internal shots | Ship this first |
| Twitter/X Cards | You still need twitter:card extras X does not infer | Duplicating OG with conflicting copy | Optional overlay, not a second site |
| No preview tags | A tiny site where the title element is enough | Ugly unfurls, not a security hole | Not a roast F |
| Share widgets | You actually want third-party buttons | Script blast radius and cookies | Different decision than meta tags |

### When Twitter Cards still win

Twitter Cards still win when you need a card type or account attribution X will not infer from OG. Open Graph still wins as the one vocabulary to maintain. Missing both is a preview-quality issue, not a headers F.

### Common mistakes

The first mistake is pointing og:image at a cookie-gated asset that unfurlers fetch without the session.

The second mistake is putting embargo titles in OG while the visible page is still a teaser.

The third mistake is treating missing twitter:card as a security finding.

### What a URL roast can prove

The free RoastMyOpsec audit does not score Open Graph, Twitter Cards, JSON-LD, RSS, or hreflang. It does score browser security headers, TLS, cookie flags, exposed paths like .git/HEAD and .env, package.json signatures, and secret-shaped JavaScript. Unfurl tags are public marketing; keep secrets out of them and confirm previews in each platform’s debugger.

### FAQ

**Do I need both OG and Twitter Cards?**

Usually OG is enough. Add twitter:card when you still see X-specific gaps. Do not maintain two conflicting stories.

**Are OG tags security headers?**

No. They are HTML metadata for previews. CSP and HSTS are the browser guardrails.

**Does RoastMyOpsec fail missing og:image?**

No. Social preview tags are not standalone Fs on the free roast.

---

## OCSP Stapling vs Must-Staple for Public Certificates

- URL: https://roastmyopsec.com/blog/ocsp-stapling-vs-must-staple
- Category: News
- Updated: 2024-11-05

OCSP stapling vs Must-Staple for public certificates is a server-sent revocation hint versus a certificate extension that demands it. Stapling (OCSP in the TLS handshake) saves clients a trip to the CA and hides those lookups from observers. Must-Staple (RFC 7633 / TLS feature) tells supporting clients to fail closed if the staple is missing. That is a reliability commitment, not a roast finding. Certificate Transparency logging is a different disclosure channel — see Expect-CT vs CT. RoastMyOpsec confirms HTTPS and the runtime trust store. It does not fetch OCSP, does not parse Must-Staple, and does not score missing staples.

### The practical difference

Stapling is configuration on the edge. Must-Staple is burned into the leaf at issuance. Turning off stapling after Must-Staple is how you lock out browsers that honor it. HSTS plus an expired leaf is a related lockout class — fix renewal first.

TLS 1.3 still can staple. Cipher-suite beauty contests are separate — see TLS 1.2 vs 1.3.

### How to choose a policy

Most brochure sites: enable stapling on the CDN if the vendor supports it and monitors staple freshness. Skip Must-Staple unless you operate a PKI team that pages on staple failures. Do not copy Must-Staple from an internal mTLS profile onto a marketing cert.

| Control | Where it lives | Failure mode | Takeaway |
| --- | --- | --- | --- |
| OCSP stapling | TLS handshake / edge | Stale staple or origin timeout | Ops win; not a roast F |
| Must-Staple | Certificate extension | Missing staple → hard fail | Optional; high commitment |
| Certificate Transparency | Public logs | Unexpected certs in CT | See Expect-CT vs CT |
| HSTS | Browser memory | Expired cert + preload | See expiry vs HSTS |

### What the roast can prove

Trust-store acceptance of the leaf you were served. Staple presence is out of scope. Confirm in the CDN dashboard. Pair with CAA if issuance is the worry, not stapling.

### Common mistakes

The first mistake is Must-Staple on a CDN that sometimes skips OCSP.

The second mistake is disabling stapling to 'fix' an intermittent 5xx.

The third mistake is treating missing staples as a roast vulnerability.

### FAQ

**Is missing OCSP stapling a vulnerability?**

It is an ops and privacy miss, not an automatic F. Clients may still check OCSP themselves.

**Should brochure sites request Must-Staple?**

Usually no until stapling is proven reliable. Missing Must-Staple is not a roast F.

**Does RoastMyOpsec check OCSP?**

No. It does not fetch OCSP responders or parse Must-Staple.

---

## noindex vs robots.txt Disallow for Public Sites

- URL: https://roastmyopsec.com/blog/noindex-vs-robots-txt-disallow
- Category: News
- Updated: 2024-10-18

noindex vs robots.txt Disallow for public sites is an indexing decision versus a crawl-budget hint: noindex asks cooperating engines not to index a URL they can still fetch, while Disallow asks them not to crawl a path at all. If a URL must stay reachable for humans, use noindex (and keep it out of the sitemap). If it should not be public, deny it at the edge — Disallow is not a lock and RoastMyOpsec will still roast juicy Disallow lines, not a missing noindex.

### The practical difference

Search Central’s block-indexing guidance is blunt: to drop a URL from results while leaving it live, the crawler must be allowed to fetch it and see noindex. Disallowing the path first can leave a stale snippet because the engine never sees the tag. That is a search problem, not a pentest.

robots.txt is a public file. Listing internal, backup, or admin-shaped prefixes as Disallow is how teams publish a tour guide. noindex on a thank-you page or a faceted URL is ordinary SEO. Confusing the two is how staging hosts get a robots.txt novel and a live index entry anyway.

Neither signal binds browsers, scrapers, or anyone who ignores robots.txt. Access control and not deploying the file remain the controls.

### How to choose based on what you shipped

Use noindex on public HTML that people need (search result pages, thin filters, thank-you screens) and omit those URLs from the sitemap. Use a short, boring robots.txt for crawler hygiene. Use auth or edge deny for anything that should not be fetched. Do not Disallow a secret and call it OPSEC.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| noindex | The URL must load for users but should not rank | Crawler must be allowed to fetch it | Indexing hint, not a lock |
| robots.txt Disallow | You want polite crawlers to skip a prefix | Public file; can block seeing noindex | Keep the list uninteresting |
| Omit from sitemap | You do not want to advertise the URL | Omission is not hiding | Sitemap is an allow-list of wants |
| Edge deny / auth | The resource should not be public | Tags will not save a 200 with secrets | This is the real control |

### When robots.txt Disallow still wins

Disallow still wins for crawl budget on infinite calendars or duplicate parameter spaces you already do not want fetched — as long as those paths are not secrets. noindex still wins whenever humans must load the page. A missing noindex on the homepage is not a finding.

### Common mistakes

The first mistake is Disallowing a URL you needed noindex on, so crawlers never see the tag.

The second mistake is using Disallow as a treasure map of internal prefixes.

The third mistake is treating either signal as access control.

### What a URL roast can prove

On a consented scan, RoastMyOpsec fetches /robots.txt and calls out Disallow lines that look like a sensitive-area tour. The free pack also covers headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets. It does not fail you for missing noindex, JSON-LD, Open Graph, RSS, hreflang, GPC, AI crawler groups, or rel=me.

### FAQ

**Will Disallow keep a page out of Google?**

Not reliably. If the URL is already known, blocking crawl can prevent the crawler from seeing noindex. Prefer noindex on live URLs you do not want indexed.

**Is robots.txt a vault?**

No. It is a public hint to well-behaved crawlers. Anyone can read Disallow. Auth and deny are the lock.

**Does the roast score missing noindex?**

No. It may roast chatty Disallow lists. It does not treat ordinary SEO tags as security Fs.

---

## NEL vs CSP Reports for Public Site Telemetry

- URL: https://roastmyopsec.com/blog/nel-vs-csp-reports-for-site-telemetry
- Category: News
- Updated: 2024-10-01

NEL vs CSP reports is a telemetry split, not a ranking of which header 'is more secure.' Network Error Logging (NEL) plus Report-To / Reporting-Endpoints tells supporting browsers to report certain network failures to a collector you name. CSP report-uri / report-to tells them to report Content-Security-Policy violations. NEL does not block attacks. CSP reports do not prove the policy is enforcing. Public marketing sites can skip NEL entirely and still have adult headers. If you enable NEL, use HTTPS collectors, watch privacy (failed URLs can be sensitive), and do not confuse a NEL 200 with a roast pass. RoastMyOpsec does not grade NEL today. It reads CSP quality on the primary HTML response and does not POST reports to your endpoint.

### The practical difference

CSP reports are policy: 'this script was not allowed.' NEL reports are connectivity: 'this request failed from the user's network.' Different dashboards, different on-call. Mixing them in one bucket without labels is how you ignore real CSP breakages.

Both can use the Reporting API. A misnamed group means silence. See report-uri vs Report-To.

### How to choose what to turn on

If you do not have a person who reads reports, do not turn NEL on. Finish enforcing CSP and a real collector for CSP first. Add NEL when you already operate RUM or edge logs and want browser-side failure samples — with a sampling rate you can afford.

| Pipe | What it describes | Security control? | Takeaway |
| --- | --- | --- | --- |
| CSP reports | Policy violations | No — telemetry for a policy | Need enforcing CSP too |
| NEL | Network failures from the client | No | Ops signal; optional |
| Server access logs | What reached you | No | Still required |
| URL roast | Public headers and paths | A punch list | Not a log pipeline |

### Privacy and OPSEC

Failed request URLs in NEL can include query strings. Keep tokens out of URLs. Do not send NEL to a vendor you would not send access logs to. report-to on HTTP is a miss.

### Common mistakes

The first mistake is NEL with max_age in the years and a collector nobody owns.

The second mistake is treating missing NEL as a failed security header checklist.

The third mistake is the same endpoint for NEL and CSP with no type field in your parser.

### FAQ

**Should every site enable NEL?**

No. It is optional ops telemetry. Enforce CSP and HSTS first. Add NEL when someone will read it.

**Does RoastMyOpsec check NEL?**

Not as a scoring gate. The free header pack is CSP, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy.

**Is NEL a vulnerability if missing?**

No. Missing NEL is not an exposure. A public collector with no access control is the OPSEC problem.

---

## llms.txt vs robots.txt on Public Marketing Sites

- URL: https://roastmyopsec.com/blog/llms-txt-vs-robots-txt
- Category: News
- Updated: 2024-08-20

llms.txt vs robots.txt on public marketing sites is two text files that talk to crawlers, not to your WAF. robots.txt (now RFC 9309) tells well-behaved crawlers which paths to skip. llms.txt is a community convention (llmstxt.org) that points language-model crawlers at Markdown or docs you are willing to summarize. Neither authenticates anyone. Disallow is not a vault — it often publishes a treasure map. Putting /admin, /.env, or staging URLs in llms.txt 'so the model understands the product' is the same miss in a new costume. RoastMyOpsec fetches /robots.txt and reads Disallow lines. It does not currently score /llms.txt, does not crawl those URLs, and does not treat a missing llms.txt as an F.

### The practical difference

robots.txt is the established crawl policy. sitemap.xml is the index of URLs you want found. llms.txt is an optional brochure for AI systems: links to docs, not secrets. humans.txt is credits. security.txt is researcher contact. Keep the jobs separate.

GEO and answer-engine optimization may want a clean llms.txt that points at public docs. That is a content decision. It is not a reason to weaken robots.txt or to list hidden paths.

### How to choose what to publish

You want classic search: keep robots.txt boring and a sitemap of public URLs. You want AI summaries of docs: llms.txt with links you already show in the footer — HTTPS, no tokens in query strings. You do not want either file to name backups, admin, or .env. Auth and edge deny remain the lock. See Disallow vs access control.

| File | Audience | Public origin | Takeaway |
| --- | --- | --- | --- |
| /robots.txt | Well-behaved crawlers | Usual; keep Disallow uninteresting | Guidance, not a lock |
| /llms.txt | AI crawlers (community) | Optional; public docs only | Not scored; not access control |
| /sitemap.xml | Index of URLs you want found | Public URLs only | See sitemap vs robots |
| security.txt | Researchers | If you will read Contact | Different job |

### When llms.txt still wins

It wins as a short map to public documentation you already publish. robots.txt still wins as the crawl policy. Missing llms.txt is healthy. Chatty Disallow lists are the OPSEC problem either way.

### What the roast can prove

Juicy Disallow lines on /robots.txt can surface. llms.txt is not scored. Confirm the files yourself. Pair with sitemap vs robots if a sitemap also listed hidden prefixes.

### Common mistakes

The first mistake is copying every Disallow into llms.txt as 'context for the model.'

The second mistake is treating User-agent: GPTBot in robots.txt as a vault.

The third mistake is requiring llms.txt as an OPSEC finding.

### FAQ

**Is a missing llms.txt a vulnerability?**

No. It is an optional content file. Missing it is not a roast F.

**Does llms.txt hide paths from attackers?**

No. Like robots.txt, it is not access control. Do not list secrets or admin URLs in it.

**Does RoastMyOpsec fetch llms.txt?**

Not as a scored path. It does read robots.txt Disallow lines on a consented scan.

---

## rel=describedby vs llms.txt for Public Sites

- URL: https://roastmyopsec.com/blog/link-rel-describedby-vs-llms-txt
- Category: News
- Updated: 2024-08-11

rel=describedby vs llms.txt for public sites is a per-resource description decision versus a site-level AI brochure decision: describedby (an IANA link relation) points a document at a description of that resource, while /llms.txt is a community convention that points language-model crawlers at Markdown you are willing to summarize. Use describedby for accessibility or catalog descriptions of this page; use llms.txt for origin-wide public docs. RoastMyOpsec does not score either as a standalone F.

### The practical difference

describedby is a registered link relation: this resource is described by that other resource. In HTML it appears as <link rel="describedby" href="..."> (distinct from aria-describedby, which points at an id in the same document). POWDER and catalog systems used it to attach descriptions without stuffing them into the body.

llms.txt lives at the site root (and optionally /llms-full.txt) and lists URLs of public documentation. RFC 9309 robots.txt remains the crawl policy. Copying Disallow prefixes into llms.txt “for context” is the same treasure-map miss as a chatty robots.txt.

Neither file authenticates. A describedby target that requires a cookie will fail for crawlers and assistive tools that fetch without the session. Keep description URLs public if you advertise them.

### How to choose based on what you shipped

Need a machine-readable description of this article or dataset: describedby to a public HTML, RDF, or text description. Want AI crawlers to prefer your docs: a short llms.txt of URLs already in the footer. Want crawl rules: robots.txt, kept boring. Do not use any of them as a vault.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| rel=describedby | This resource has a separate public description | Pointing at a cookie-gated or unpublished URL | Per-resource description link |
| llms.txt | You want a site-level list of public docs for models | Listing unpublished or sensitive prefixes | Optional brochure, not a lock |
| robots.txt | You want crawl policy for well-behaved agents | Disallow as a tour guide | RFC 9309 guidance |
| aria-describedby | The description is already in this page’s DOM | Confusing it with a link relation to another URL | Same-document a11y, not llms.txt |

### When rel=describedby still wins

describedby still wins for a dataset or article that has a canonical public description on another URL. llms.txt still wins as a one-file map of docs you already publish. Missing both is healthy. Chatty inventories of internal paths are the miss in any of these files.

### Common mistakes

The first mistake is pointing describedby at a description URL that is not actually public.

The second mistake is copying robots.txt Disallow paths into llms.txt as model context.

The third mistake is treating a missing llms.txt or describedby link as a security finding.

### What a URL roast can prove

RoastMyOpsec does not score rel=describedby, llms.txt, JSON-LD, Open Graph, or AI crawler groups as standalone Fs. It does read robots.txt Disallow lines and still runs the free pack: headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets. Keep description links on public documents you already meant to share.

### FAQ

**Is describedby the same as aria-describedby?**

No. aria-describedby references an element id in the same document. rel=describedby is a link to another resource.

**Can llms.txt replace robots.txt?**

No. robots.txt is crawl policy. llms.txt is an optional content map for models.

**Does RoastMyOpsec fetch llms.txt?**

Not as a scored path. Missing llms.txt is not an F. Juicy robots.txt Disallow lines can still surface.

---

## Last-Modified vs Sitemap lastmod for Public Sites

- URL: https://roastmyopsec.com/blog/last-modified-vs-sitemap-lastmod
- Category: News
- Updated: 2024-08-03

Last-Modified vs sitemap lastmod for public sites is a cache-validator decision versus a crawler-hint decision: Last-Modified (and ETag) help clients skip a body they already have, while <lastmod> in sitemap.xml tells cooperating crawlers when a listed URL changed. Keep both dates honest. Neither is access control, and RoastMyOpsec does not score sitemap lastmod or Last-Modified as a standalone F.

### The practical difference

Last-Modified is an HTTP response header. With If-Modified-Since, a client may get 304. It is a caching and bandwidth tool. Sitemap lastmod is XML inside a sitemap the crawler already decided to fetch. Google has said lastmod is useful when it matches reality and ignored when it is stamped “now” on every URL.

A sitemap is an advertisement of URLs you want found. Putting lastmod on a path that should not be public does not hide it; it invites recrawl. Last-Modified on a 200 for .env is still a 200 for .env. Fix exposure, do not timestamp it.

Personalized pages should not be publicly cacheable just because Last-Modified looks old. Cache-Control owns that story.

### How to choose based on what you shipped

Set Last-Modified or ETag on static public assets if your origin or CDN can do so cheaply. Set sitemap lastmod only when the URL’s meaningful content changed. Omit lastmod rather than lie. Do not list non-public URLs in the sitemap to “keep lastmod in sync.”

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Last-Modified / ETag | Static public files and cache revalidation | Personalized HTML marked public | HTTP caching, not hiding |
| Sitemap lastmod | The listed public URL actually changed | Now-stamping every row | Honesty or omit |
| IndexNow / recrawl APIs | You need to notify supporting engines | Notifying URLs that should not be public | Notification, not a lock |
| robots.txt Disallow | Polite crawlers should skip a prefix | Treasure-map Disallow lists | Guidance, not lastmod |

### When Last-Modified still wins

Last-Modified still wins for CDN-cached static assets and simple documents. Sitemap lastmod still wins as a recrawl hint when it is true. Missing either is fine. Lying lastmod and exposing sensitive paths are the misses, and only the latter is roast material.

### Common mistakes

The first mistake is stamping sitemap lastmod to now on every URL so crawlers learn to ignore you.

The second mistake is putting lastmod on URLs that should not be in a public sitemap at all.

The third mistake is treating a missing Last-Modified header as a security finding.

### What a URL roast can prove

RoastMyOpsec may roast a robots.txt that advertises sensitive prefixes, and it probes a short list of sensitive paths. It does not score Last-Modified, sitemap lastmod, IndexNow, JSON-LD, or hreflang as standalone Fs. The free pack is headers, TLS, cookies, those path signatures, and js-secrets. Confirm sitemaps in Search Console.

### FAQ

**Does Google require lastmod?**

No. It is optional and only helpful when it matches real changes. Fake dates are worse than omitting it.

**Does Last-Modified hide a file?**

No. It is a validator for a response you already served.

**Does RoastMyOpsec grade sitemaps?**

Not as a lastmod QA. Chatty robots.txt Disallow lines can still surface. Hidden-path sitemaps are a separate OPSEC topic.

---

## JSON-LD vs Microdata for Public Sites

- URL: https://roastmyopsec.com/blog/json-ld-vs-microdata-for-public-sites
- Category: News
- Updated: 2024-07-08

JSON-LD vs Microdata for public sites is an encoding decision for the same Schema.org types: JSON-LD is a script block you can maintain beside the template, while Microdata weaves itemprop attributes into the visible HTML. Google Search Central prefers JSON-LD for most sites. Structured data is not access control, and RoastMyOpsec does not score missing JSON-LD as a standalone F.

### The practical difference

JSON-LD is a JSON document in <script type="application/ld+json">. It can describe Organization, WebSite, Article, BreadcrumbList, and FAQ without touching every heading. Microdata uses itemscope, itemtype, and itemprop on existing elements so the markup and the visible text stay tied together.

The OPSEC miss is the same in both encodings: stuffing internal emails, office floor maps, or unpublished product SKUs into structured data “for rich results.” Schema.org is a public graph. If you would not put it in the footer, do not put it in JSON-LD.

RDFa exists as a third encoding. For a marketing Next.js or CMS site, JSON-LD is the path of least drama. Microdata still appears in older themes. Do not ship both with conflicting names and URLs.

### How to choose based on what you shipped

Choose JSON-LD unless a theme already has accurate Microdata you do not want to fork. Keep types aligned with visible content. Skip Review spam and fake breadcrumbs. Organization and WebSite on the homepage are enough for most brochure origins.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| JSON-LD | You want one maintainable graph next to the template | Easy to drift from visible copy | Default for public marketing sites |
| Microdata | The theme already annotates visible nodes correctly | Messy with components and hydration | Fine if it already matches the page |
| No structured data | A tiny brochure with nothing to describe | Not a security failure | Optional for OPSEC |
| Both encodings | Almost never | Conflicting names, URLs, and types | Pick one source of truth |

### When Microdata still wins

Microdata still wins on a long-lived theme where itemprop already mirrors the visible byline and you would create drift by adding a second graph. JSON-LD still wins for new pages, FAQs, and anything a component library would butcher as attributes. Missing both is not a roast F.

### Common mistakes

The first mistake is duplicating JSON-LD and Microdata with different canonical URLs or brand names.

The second mistake is putting non-public emails or internal hostnames into Schema.org.

The third mistake is treating missing JSON-LD as a security finding.

### What a URL roast can prove

RoastMyOpsec does not parse or grade JSON-LD, Microdata, Open Graph, RSS, or hreflang as standalone Fs. The free roast is headers, TLS, cookies, bounded sensitive paths (.git/HEAD, .env, package.json signatures), and js-secrets on a URL you assert you own. Structured data is a content graph; keep secrets out of it and confirm rich results in Search Console.

### FAQ

**Does Google prefer JSON-LD or Microdata?**

Google documents JSON-LD as the recommended format for most structured data. Microdata still works if it is correct and visible.

**Is missing Schema.org a vulnerability?**

No. It is optional public metadata. Missing it is not a roast F.

**Can JSON-LD hide information?**

No. It is in the document anyone can fetch. Do not put unpublished facts in the graph.

---

## IPv6 AAAA vs Origin IP Exposure on CDNs

- URL: https://roastmyopsec.com/blog/ipv6-aaaa-vs-origin-ip-exposure
- Category: News
- Updated: 2024-06-30

IPv6 AAAA vs origin IP exposure on CDNs is the same bypass class as a leftover A record, in 128-bit clothing. Visitors should resolve your public hostname to the CDN or anycast edge. If AAAA (or A) still points at the origin VPS, clients and scanners can skip the WAF. Alt-Svc that names an origin IP is a cousin. Dangling CNAMEs are another. IPv6 is not a vulnerability; a public AAAA to the origin is a map. RoastMyOpsec inspects landing headers for banners and CDN fingerprints. It does not query DNS A/AAAA, does not probe origin IPs, and does not score missing IPv6 as an F.

### The practical difference

A and AAAA are how browsers find an address. The origin should accept traffic only from the CDN. The public DNS should not publish that origin address on the brand name — or on a guessable origin.example.com. See CDN fingerprints vs origin exposure and Alt-Svc vs origin IP.

Enabling IPv6 on the edge is fine and often good. Enabling IPv6 only on the origin while the hostname still has AAAA is how you split-brain the protection.

### How to choose DNS

Public hostname: A and AAAA to the CDN, matching www vs apex policy. Origin: firewall to CDN egress, no public AAAA on a name people will guess. Dual-stack edge: keep both families on the CDN, not one family on the origin. Do not copy origin AAAA into the zone 'so IPv6 works' without the CDN in front.

| Record / header | Healthy shape | Miss | Takeaway |
| --- | --- | --- | --- |
| AAAA on brand name | CDN / anycast | Origin VPS address | Same class as a leaked A |
| A on brand name | CDN | Origin IPv4 | See CDN vs origin guide |
| Alt-Svc | h3 on the public name | Origin IP in the advertisement | See Alt-Svc vs origin IP |
| Dangling CNAME | Remove or claim | Abandoned SaaS | See dangling CNAME |

### When IPv6 still wins

IPv6 on the CDN hostname wins for reachability. Origin AAAA on the brand name never wins. Missing AAAA on a brochure is a product choice, not a roast F. Pair with www vs apex so you do not publish the loser hostname as a secret IPv6 origin.

### What the roast can prove

Banner and CDN-shaped headers on the HTML response can surface. DNS is out of scope. Confirm A/AAAA in your DNS console. Pair with dangling CNAMEs if a leftover name still resolves.

### Common mistakes

The first mistake is adding AAAA for the origin because IPv4 is already proxied and IPv6 'was forgotten at the CDN.'

The second mistake is origin.example.com with a public AAAA and no firewall.

The third mistake is treating missing IPv6 as an OPSEC vulnerability.

### FAQ

**Is IPv6 a vulnerability?**

No. A public AAAA that points at the origin instead of the CDN is the miss.

**Must marketing sites publish AAAA?**

Only if the edge actually serves IPv6. Missing AAAA is not a roast F.

**Does RoastMyOpsec query AAAA?**

No. It does not look up DNS or probe origin IPs.

---

## Integrity-Policy vs Subresource Integrity for CDN Scripts

- URL: https://roastmyopsec.com/blog/integrity-policy-vs-sri
- Category: News
- Updated: 2024-06-21

Integrity-Policy vs Subresource Integrity for CDN scripts is a header that demands hashes versus the hashes themselves. SRI is integrity= on a <script> or <link>. Integrity-Policy (and Integrity-Policy-Report-Only) tells supporting browsers that specified destinations — typically script — must include integrity, and can report violations. The header does not pin a CDN URL by itself. An integrity attribute without a matching file still fails closed in the browser. Unpinned third-party scripts remain the miss. RoastMyOpsec reads public HTML for cross-origin scripts without integrity. It does not score Integrity-Policy, does not recompute hashes, and does not attack the CDN.

### The practical difference

SRI is per resource. Forget one tag and that script is unpinned. Integrity-Policy is a floor: supporting browsers can refuse or report scripts that shipped without integrity. CSP hashes and nonces are a different allowlist — they name content or a nonce, not a CDN file hash. See CSP nonces vs hashes.

Report-Only Integrity-Policy is telemetry, like CSP-Report-Only. Do not treat reports as a lock.

### How to choose a rollout

First-party bundles: prefer serving from your origin so SRI is optional. Third-party CDNs you must keep: add integrity on every tag, then Integrity-Policy-Report-Only, then enforce when the report bucket is quiet. Do not enforce while a tag manager injects unsigned scripts.

| Control | What it pins | Browser support note | Takeaway |
| --- | --- | --- | --- |
| integrity= on the tag | That one URL's bytes | Widely deployed | The actual pin |
| Integrity-Policy | That destination must have a pin | Newer; not universal | Floor, not a hash |
| CSP script-src hash/nonce | Inline or allowed sources | Widely deployed | Complement; see nonce vs hash |
| Unpinned CDN src | Nothing | Works until the file changes | The miss the roast looks for |

### What the roast can prove

Cross-origin scripts without integrity can surface as findings. Missing Integrity-Policy is not a finding. A present Integrity-Policy with still-unpinned tags is a policy the browser may enforce later — fix the tags. Pair with GitHub Actions if the hash is generated in CI.

### Common mistakes

The first mistake is Integrity-Policy enforce while GTM still injects scripts.

The second mistake is copying an integrity hash from a blog post and never regenerating it in CI.

The third mistake is treating Integrity-Policy as a replacement for CSP.

### FAQ

**Does Integrity-Policy replace SRI attributes?**

No. It requires them (in supporting browsers). The hash still lives on the tag.

**Should every marketing site set Integrity-Policy?**

After every third-party script you keep has integrity. Report-Only first. Missing the header is not a roast F.

**Does RoastMyOpsec score Integrity-Policy?**

No. It looks for missing integrity on cross-origin scripts in public HTML.

---

## IndexNow vs Google Sitemap Ping for Public Sites

- URL: https://roastmyopsec.com/blog/indexnow-vs-google-sitemap-ping
- Category: News
- Updated: 2024-06-13

IndexNow vs Google sitemap ping for public sites is a crawl-notification decision, not a security control: IndexNow POSTs URLs you already publish to supporting engines, while Google stopped honoring the old sitemap ping endpoint. Keep a boring sitemap of public URLs either way, and treat lastmod as an honesty field, not a lock. RoastMyOpsec does not score IndexNow, key files, or ping success as a standalone F.

### The practical difference

Google’s sitemap ping was a GET to a ping URL that asked Google to recrawl a sitemap. Search Central retired that path in 2023; submitting or hosting the sitemap, plus lastmod that matches reality, is what remains. IndexNow is a separate protocol: you host a key file and notify participating engines when a public URL changes.

Neither protocol authenticates visitors or hides a path. A key file at the site root is supposed to be fetchable. Putting staging hosts, signed preview URLs, or backup prefixes into an IndexNow payload is the same miss as listing them in a sitemap. Search engines that never joined IndexNow still need sitemaps and ordinary crawl.

OPSEC here is boring inventory: only notify URLs that already belong in the public sitemap. Do not treat a 200 from an IndexNow endpoint as proof the origin is hardened.

### How to choose based on what you shipped

If Bing, Yandex, or other IndexNow participants matter to the marketing host, ship a key file and notify only public article and product URLs after publish. If Google is the engine you actually care about, skip ping nostalgia and keep sitemap.xml accurate. If you sell nothing but brochure pages, a sitemap plus CDN cache busting is enough; IndexNow is optional.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| IndexNow | You want faster recrawl at participating engines | Key file and payload must stay public-only | Notification, not a vault |
| Google sitemap ping | It does not — the ping endpoint is retired | Scripts still hitting the old ping URL waste time | Host the sitemap; skip the ping |
| Sitemap lastmod | URLs actually changed on that date | Fake lastmod trains crawlers to ignore you | Honesty beats ping theater |
| robots.txt + sitemap | You need crawl policy and an index of wanted URLs | Disallow is not access control | Keep both boring and public |

### When IndexNow still wins

IndexNow still wins when you publish often on a host that IndexNow engines actually crawl, and you can automate notifications without stuffing preview or authenticated URLs into the list. It never wins as a substitute for HTTPS, headers, or keeping .env off the origin.

### Common mistakes

The first mistake is treating a retired Google ping URL as a live ranking lever.

The second mistake is notifying IndexNow of URLs you would not put in a public sitemap.

The third mistake is scoring a missing IndexNow key as a security failure.

### What a URL roast can prove

The free RoastMyOpsec audit reads security headers, TLS on the landing URL, cookie flags, bounded path probes (.git/HEAD, .env, package.json signatures), and JavaScript secret-shaped strings. It does not score IndexNow, sitemap ping, JSON-LD, Open Graph, RSS, hreflang, GPC, AI crawler groups, or rel=me as standalone Fs. Confirm IndexNow yourself; use the roast for origin hygiene.

### FAQ

**Does Google still support sitemap ping?**

No. Google Search Central retired sitemap ping. Host a sitemap and keep lastmod accurate instead of calling the old ping endpoint.

**Is a missing IndexNow key a vulnerability?**

No. IndexNow is optional crawl notification. A missing key is not a roast F and is not access control.

**Does RoastMyOpsec check IndexNow?**

No. The free roast does not score IndexNow or sitemap ping. It still flags juicy robots.txt Disallow lines and exposed paths on a consented URL.

---

## humans.txt vs security.txt on Public Marketing Sites

- URL: https://roastmyopsec.com/blog/humans-txt-vs-security-txt
- Category: News
- Updated: 2024-05-18

humans.txt vs security.txt on public marketing sites is two small text files with different jobs. humans.txt (usually /humans.txt) is an informal credits list: names, roles, tools, sometimes a 'last updated' line. It is branding, not a control. security.txt lives at /.well-known/security.txt (RFC 9116) and tells researchers where to send a report. Mixing them is how teams paste a personal email into humans.txt and ignore Contact in security.txt — or list an intern in credits who still has production SSH. Neither file authenticates anyone or hides /.env. RoastMyOpsec does not require humans.txt or security.txt and does not score a missing credits file as an F.

### The practical difference

security.txt is machine-readable disclosure: Contact, Expires, optional Policy. humans.txt is a thank-you note. OpenID discovery and the web app manifest are other public catalogs. Do not put credentials, internal hostnames, or 'we use this exact CMS version' inventories in humans.txt.

A /security HTML page is still useful for humans. See security.txt vs a public contact page.

### How to choose what to publish

If you will read reports: ship security.txt with a mailbox you monitor. humans.txt is optional; keep it to first names or roles if you publish it, not home addresses. If legal needs a policy URL, put it in security.txt Policy, not only in a poem at /humans.txt. Do not list a personal Gmail you will abandon in either file.

| File | Job | Typical miss | Takeaway |
| --- | --- | --- | --- |
| /.well-known/security.txt | Researcher intake | Stale Contact; no Expires | Publish if you will actually read it |
| /humans.txt | Credits / culture | Personal emails, stack résumé | Optional; keep it bland |
| openid-configuration | IdP metadata | Internal issuer URLs | See OpenID vs security.txt |
| Web app manifest | PWA install metadata | Over-sharing app names | See manifest vs public metadata |

### When humans.txt still wins

It wins as a light credits page with no PII and no version pins. security.txt always wins as the disclosure channel. Missing humans.txt is healthy. Missing security.txt is a missed inbox, not a roast F.

### What the roast can prove

Neither file is a scored vulnerability miss. Confirm Contact yourself. Pair with consent-based scanning: do not roast third-party sites as 'research' just because their humans.txt listed a name.

### Common mistakes

The first mistake is putting the security mailbox only in humans.txt where parsers will not look.

The second mistake is a humans.txt that names every vendor and CMS version.

The third mistake is treating a missing humans.txt as an OPSEC finding.

### FAQ

**Do I need humans.txt for SEO or security?**

No. It is optional credits. security.txt is the disclosure file researchers look for.

**Can humans.txt replace security.txt?**

No. RFC 9116 parsers look at /.well-known/security.txt. Credits will not be found as Contact.

**Does RoastMyOpsec require either file?**

No. Missing humans.txt is not an F. security.txt is also not required to pass.

---

## HTTP/2 vs HTTP/3 for Public Marketing Sites

- URL: https://roastmyopsec.com/blog/http2-vs-http3-for-public-sites
- Category: News
- Updated: 2024-05-01

HTTP/2 vs HTTP/3 for public marketing sites is which application protocol the edge offers after TLS, not whether the URL is HTTPS. HTTP/2 (h2) multiplexes streams over TLS/TCP. HTTP/3 (h3) multiplexes over QUIC/UDP. Both are performance and reliability choices. Neither is a security header, a WAF, or a substitute for TLS 1.3 and HSTS. Enable h2 everywhere the CDN supports it. Enable h3 on the same public hostname when the edge actually speaks it, advertised with a boring Alt-Svc — not an origin IP. RoastMyOpsec confirms HTTPS and the runtime trust store. It does not score missing HTTP/3, does not speak QUIC as a finding, and does not probe origin IPs.

### The practical difference

HTTP/1.1 is still the compatibility floor. HTTP/2 is the default modern TCP stack. HTTP/3 helps on lossy mobile networks because one lost packet does not stall every stream the way TCP head-of-line blocking can. TLS 1.2 vs 1.3 is the handshake generation; h2/h3 is the HTTP generation. You can have TLS 1.3 with HTTP/2 only.

Alt-Svc is how browsers learn h3 exists for this name. Keep that advertisement on the CDN hostname. See Alt-Svc vs origin IP exposure.

### How to choose a policy

Brochure on a major CDN: leave HTTP/2 on, turn on HTTP/3 if the dashboard offers it and UDP 443 is not blocked in your enterprise clients. Government or locked-down corp browsers: keep HTTP/2; missing h3 is not an incident. Do not terminate HTTP/3 on a second hostname that skips the WAF. UDP to the origin should stay firewalled; only the edge speaks QUIC to the internet.

| Protocol | Typical 2026 role | OPSEC note | Takeaway |
| --- | --- | --- | --- |
| HTTP/1.1 | Compatibility | Still fine behind h2 | Do not require it for the roast |
| HTTP/2 | Default on public edges | Not a scored finding | Enable it |
| HTTP/3 | Optional speed on the same name | Alt-Svc must not name an origin IP | Nice; not a roast F if absent |
| TLS 1.3 | Handshake generation | Orthogonal to h2 vs h3 | See TLS 1.2 vs 1.3 |

### When HTTP/3 wins

HTTP/3 wins for mobile-heavy marketing traffic on a CDN that already filters UDP 443. HTTP/2 wins as the must-have modern baseline. TLS 1.3 wins independently. 0-RTT on QUIC is an ops/product setting: do not enable it for state-changing requests if you do not understand replay; that is a configuration caution, not a roast check.

### What the roast can prove

Scheme and trust-store acceptance. HTTP version, Alt-Svc, and QUIC are out of scope. Confirm in the CDN dashboard. Pair with CDN fingerprints if the Via or Server header still names the origin stack.

### Common mistakes

The first mistake is advertising h3 in Alt-Svc while the edge still only speaks HTTP/2.

The second mistake is opening UDP 443 to the origin so clients can skip the CDN.

The third mistake is treating missing HTTP/3 as an OPSEC vulnerability.

### FAQ

**Is HTTP/2 a vulnerability?**

No. It is the usual modern default. HTTP/1.1-only is slower, not an automatic roast F.

**Must marketing sites enable HTTP/3?**

Only if the edge supports it on the public name. Missing h3 is not a roast finding.

**Does RoastMyOpsec score HTTP/3?**

No. It does not treat protocol generation as a vulnerability and does not probe origin IPs.

---

## HTTP 103 Early Hints vs Content-Security-Policy

- URL: https://roastmyopsec.com/blog/http-103-early-hints-vs-csp
- Category: News
- Updated: 2024-04-14

HTTP 103 Early Hints vs Content-Security-Policy is a speed header versus an allowlist. 103 (RFC 8297) lets a CDN or origin send Link: rel=preload (and similar) while the document is still generating. That is not a substitute for CSP, HSTS, or frame-ancestors on the final 200. Modern Chromium applies the document CSP to early-hinted subresources; do not assume every browser or every proxy does. Do not put secrets or internal hostnames in Link URLs — those leave in the first bytes. RoastMyOpsec grades CSP and other headers on the landing response. It does not score 103, does not follow preload Links as a crawl, and does not treat missing Early Hints as a finding.

### The practical difference

Early Hints are a hint: 'you will probably need these URLs.' CSP is a rule: 'scripts may only come from here.' Preload without CSP still loads whatever the Link named if the document would have loaded it anyway. CSP without 103 is still a policy. Use both only if you need the latency win.

dns-prefetch and prefetch are cousins. They can leak destinations in headers the same way. See dns-prefetch vs Referrer-Policy.

### How to choose what to send on 103

Preload only public, first-party assets you already allow in CSP (script-src / style-src). Skip authenticated URLs, signed query strings, and admin hosts. If the CDN cannot attach the same CSP to the final response, skip Early Hints until it can.

| Signal | When it fires | Policy role | Takeaway |
| --- | --- | --- | --- |
| 103 Link preload | Before the document body | None by itself | Speed only; no secrets in URLs |
| CSP on 200 | With the HTML | Allowlist for scripts and more | Required; see CSP vs XFO |
| CSP Report-Only | With the HTML | Telemetry, not a lock | Do not preload against a policy you have not enforced |
| HSTS | After a trusted HTTPS response | HTTPS memory | Unrelated to 103; still set it |

### What the roast can prove

A roast can show CSP (or Report-Only) on the final response. Absence of 103 is not a miss. A 103 that preloads a third-party script you then block in CSP is wasted bandwidth, not a roast finding. Confirm Link URLs in your CDN config.

### Common mistakes

The first mistake is preloading a CDN script URL that is not in script-src.

The second mistake is putting a signed asset URL with a token in Link.

The third mistake is treating 103 as 'security headers sent early' and omitting CSP on the 200.

### FAQ

**Does Early Hints bypass CSP?**

It must not in browsers that apply document CSP to hinted loads. Still put CSP on the final response. Do not rely on 103 for isolation.

**Should every marketing site enable 103?**

Only if the CDN supports it and the preloaded URLs are public first-party assets already allowed by CSP. Missing 103 is not an OPSEC F.

**Does RoastMyOpsec score HTTP 103?**

No. It does not treat missing Early Hints as a finding and does not crawl preload Links.

---

## hreflang vs Canonical on Single-Language Public Sites

- URL: https://roastmyopsec.com/blog/hreflang-vs-canonical-single-language
- Category: News
- Updated: 2024-03-03

hreflang vs canonical on a single-language public site is an international-targeting decision versus a duplicate-URL decision: skip hreflang until you ship real language or region variants, and use canonical (plus 301s) for www, parameters, and copies. Fake hreflang clusters confuse crawlers and sometimes advertise locale hosts that are still staging. RoastMyOpsec does not score hreflang or canonical as standalone Fs.

### The practical difference

rel=canonical says “this live URL is a duplicate of that preferred URL.” hreflang says “these URLs are alternate language or region versions of each other.” Google’s international docs require reciprocal annotations. A single-language brochure with hreflang="en" pointing at itself plus a phantom hreflang="de" 404 is noise.

Canonical is still useful on a one-locale site for query-string copies and CMS aliases. Hostname consolidation remains a 301. Mixing hreflang into that problem is how teams invent /de/ that noindex-fails while the sitemap lists it.

Locale subdomains are public origins. If de.example.com is not ready, do not advertise it in hreflang. That is inventory OPSEC, not a header finding.

### How to choose based on what you shipped

One language, one host: no hreflang. Canonical self-tags optional; 301 the duplicate hosts. Multiple languages you actually serve: reciprocal hreflang, unique URLs, and canonicals that do not smash locales into one page. x-default is for a language chooser you really ship, not a decorative tag.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| No hreflang | You only publish one language | Copy-pasting hreflang from a tutorial | Default for single-language sites |
| rel=canonical | Duplicates exist on the same locale | Canonicalizing locales into one URL | Duplicates, not translations |
| hreflang cluster | You ship real localized URLs | One-way links and staging hosts in the cluster | Reciprocal and live only |
| 301 host consolidation | www, apex, or http should not linger | Using hreflang instead of a redirect | Move the host; do not annotate it |

### When hreflang still wins

hreflang still wins the week you actually launch a second locale with crawlable pages and matching return tags. On a single-language site it does not win. Canonical still wins for true duplicates. Missing hreflang is the correct empty state.

### Common mistakes

The first mistake is adding hreflang on a one-locale site because a checklist said so.

The second mistake is listing locale hosts in hreflang that are staging or unpublished.

The third mistake is using canonical to collapse different languages into one URL.

### What a URL roast can prove

The free roast does not grade hreflang, canonical, JSON-LD, Open Graph, or RSS as standalone Fs. It does inspect security headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets on the URL you consented to scan. Keep locale inventory honest; do not advertise unfinished hosts in public annotations.

### FAQ

**Do I need hreflang on an English-only site?**

No. Add it when you publish real alternate language or region URLs that return 200.

**Can hreflang replace canonical?**

No. They answer different questions. Translations are hreflang; duplicates are canonical or 301.

**Does RoastMyOpsec score hreflang?**

No. It is not a standalone F. Confirm clusters in Search Console if you ship them.

---

## How to Read an OPSEC Score (1–10) on a Public URL

- URL: https://roastmyopsec.com/blog/how-to-read-an-opsec-score
- Category: News
- Updated: 2024-02-23

How to read an OPSEC score (1–10) on a public URL: start at the top of the scale and subtract for evidence-backed misses on that response — headers, TLS, cookies, exposed paths, client-side secrets, and the rest of the catalog that actually fired. A 10 means the scanner did not land those deductions, not that the company is unhackable. A 4 means the public costume is leaking. RoastMyOpsec publishes free-audit scores on a leaderboard when you allow it. The number is a roast, not a compliance certificate.

### The practical difference

A score is a summary of what this scan could prove from public HTTP, DNS, and optional GitHub. It is not PCI, SOC 2, or a red-team report. Passing findings do not add bonus points; they avoid penalties.

Teasers and inconclusive checks (for example a session cookie issued only after login) should not be treated as a silent 10 on that control.

### How to choose what to fix from the number

Read the unlocked findings first. Critical and high evidence-backed items move the score more than info. Fix exposed secrets and VCS paths before arguing about Server banners. Re-scan the URL you own after the edge config changes.

| Band | Usual story | First move | Takeaway |
| --- | --- | --- | --- |
| High 8–10 | Public surface is mostly adult | Still open the vault; banners and trackers hide there | Not a pentest pass |
| Mid 5–7 | Several real misses | Headers, cookies, mixed content, robots | Punch list, not panic |
| Low 1–4 | Secrets, VCS, or cleartext-class problems | Rotate, deny paths, force HTTPS | Treat as an incident rehearsal |
| No score / unreachable | Scanner could not gather evidence | Confirm the URL is public and owned | Not an OPSEC F |

### Leaderboard vs private report

The weekly board is highest scores among published free audits. Shame is a feature. If you do not want a hostname listed, do not publish that scan. The report URL is still the place to work the findings.

Website vs app mode can change which catalog items apply. Do not compare an app origin to a brochure site as if they were the same exam.

### Common mistakes

The first mistake is screenshotting 9.4 and skipping the blurred vault.

The second mistake is comparing scores across different dates without re-running the same URL.

The third mistake is treating a demo or staging host as production OPSEC.

### FAQ

**Is a 10/10 OPSEC score a security guarantee?**

No. It means this public-surface roast did not deduct for the evidence it could see. Authenticated bugs, business logic, and private networks are out of scope.

**Why did my score change after I only fixed CSP?**

Each evidence-backed miss has a different weight. One header can move the needle; an exposed .env moves it more. Re-scan to see the new total.

**Does the leaderboard include vault-only findings?**

Published free audits use the free-scan score. Unlocking the vault is for the full roast copy, not for bragging rights on a different exam.

---

## Host Header vs SNI on Shared TLS for Public Sites

- URL: https://roastmyopsec.com/blog/host-header-vs-sni-on-shared-tls
- Category: News
- Updated: 2024-02-06

Host header vs SNI on shared TLS for public sites is a name-binding decision: SNI (RFC 6066) tells the TLS stack which certificate to present, while the HTTP Host header (RFC 9110) tells the origin which virtual host to serve after the handshake. They should name the same public site. RoastMyOpsec’s free TLS check proves the audited URL is HTTPS with a runtime-trusted certificate; it does not prove SNI/Host isolation, HTTP/2 coalescing edge cases, or that another tenant cannot be requested on the same IP.

### The practical difference

On shared CDNs and multi-tenant load balancers, one IP serves many certificates. The client sends SNI during the handshake so the edge can pick a cert. After TLS, HTTP/1.1 and HTTP/2 still send Host (or :authority) so the application picks the site. If SNI says a.com and Host says b.com, you are in misconfiguration or request-smuggling-adjacent territory that belongs to your platform vendor’s hardening — not a DIY exploit write-up.

Default vhosts are how teams leak the wrong site or a boilerplate “welcome to nginx” on a raw IP. That is inventory. HSTS and a single canonical host (www versus apex) still matter so users do not sit on the wrong name. A certificate that covers too many unrelated names is a brand and phishing-confusion issue as much as a TLS one.

This is not a reason to scan other people’s Host values. Roast the origin you own. Confirm in your CDN that unknown Hosts get a 404, not a neighbor’s app.

### How to choose based on what you shipped

Pick one canonical hostname, put it on the cert and in SNI, redirect the rest, and set Host/authority to match. On a shared TLS service, use the vendor’s documented vhost isolation and refuse unknown hosts. Do not debug this by pointing tools at tenants you do not operate.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Matching SNI + Host | Every public site you operate | Default vhost serving a neighbor or a banner | Same name in handshake and HTTP |
| Shared TLS / CDN | You want certs without owning the IP | Unknown Host falling through to another tenant | Configure isolation; do not DIY attacks |
| Canonical 301 + HSTS | www, apex, and http should collapse | Cert missing the name you redirect to | One name users should keep |
| Raw IP in the browser | It should not win for a marketing site | Default site or cert mismatch pages | Do not advertise the origin IP |

### When SNI still wins

SNI still wins as the only practical way to host many TLS names on one IP. The Host header still wins as the HTTP vhost key. A URL roast still wins as a check that your chosen URL is HTTPS — not as a multi-tenant isolation proof. Missing SNI on ancient clients is a compatibility tale; modern public sites assume SNI.

### Common mistakes

The first mistake is letting unknown Host headers fall through to a default site on shared TLS.

The second mistake is treating a green padlock on one name as proof every neighbor on the IP is isolated.

The third mistake is expecting RoastMyOpsec to certify SNI versus Host matching as a standalone F.

### What a URL roast can prove

The free TLS insight records whether the consented URL is served over HTTPS (or redirected to it) with a certificate the runtime trust store accepts. It does not score SNI versus Host mismatches, JSON-LD, Open Graph, RSS, hreflang, GPC, AI crawler groups, or rel=me as standalone Fs. Pair this with canonical host redirects. The rest of the free pack is headers, cookies, path probes (.git/HEAD, .env, package.json signatures), and js-secrets.

### FAQ

**Is SNI a replacement for the Host header?**

No. SNI selects a certificate at TLS time. Host or :authority selects the HTTP site after the handshake. They should agree.

**Does a URL roast prove shared-IP isolation?**

No. It proves HTTPS on the URL you pasted. Tenant isolation is a platform configuration review.

**Should I scan other Host names on my CDN IP?**

Only hosts you control, with consent. Unsolicited probing of neighbors is out of scope for this product.

---

## GPC vs DNT Privacy Signals for Public Sites

- URL: https://roastmyopsec.com/blog/gpc-vs-dnt-privacy-signals
- Category: News
- Updated: 2024-01-20

GPC vs DNT for public sites is a privacy-signal decision: DNT (Do Not Track) is a legacy header most sites ignore, while Global Privacy Control (Sec-GPC) is the browser signal many US state privacy laws treat as a legally meaningful opt-out of sale or sharing. Honor GPC in your consent and ad stack if those laws apply. RoastMyOpsec does not score GPC or DNT as a standalone F; it is not a CMP auditor.

### The practical difference

DNT: 1 asked sites not to track. There was never a consistent legal or industry requirement to obey it, and the W3C Tracking Preference Expression work did not become universal practice. GPC is a newer signal (Sec-GPC: 1 and a JS navigator.globalPrivacyControl) defined so that a user agent can express an opt-out that maps onto “do not sell/share” style rules.

Honoring GPC is a product and legal implementation: your tag manager, ad partners, and server-side analytics have to actually change behavior. Sending a marketing pixel anyway is the miss. DNT can still appear; treating it as equivalent to GPC without counsel is how teams write a privacy page that disagrees with the network tab.

These signals are not security headers like CSP. They do not stop XSS. They are preference flags. First-party analytics with a real opt-out is still a different architecture than a third-party tag blob.

### How to choose based on what you shipped

If you run ads or “sale/share” of personal information into regions where GPC is recognized, wire Sec-GPC into the same path as your privacy-choice UI. If you only use first-party, cookieless, aggregate analytics, document that and still do not ignore GPC where the law says it counts. Do not advertise “we honor DNT” if the stack never did.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Honor GPC (Sec-GPC) | You sell or share personal info in GPC-relevant jurisdictions | UI says opted out while pixels still fire | Wire it into the real consent path |
| DNT | Almost never as a sole control | Privacy policy that claims DNT without behavior | Legacy signal, weak practice |
| First-party analytics | You can answer product questions without ad pixels | Reintroducing a tag manager “just for one test” | Smaller blast radius |
| Ignore both | You have no tracking to honor — rare on marketing sites | Still loading third-party ad scripts | The scripts are the story |

### When DNT still wins

DNT still wins only as a historical header you might log. GPC still wins as the signal to implement if sale/share opt-outs apply. Missing GPC handling is a privacy-program gap, not a RoastMyOpsec headers F. A site with no third-party pixels has less to honor.

### Common mistakes

The first mistake is claiming you honor DNT or GPC in the privacy page while the tag manager ignores the header.

The second mistake is treating DNT as legally equivalent to GPC without checking the rules that apply to you.

The third mistake is expecting a URL roast to certify Global Privacy Control compliance.

### What a URL roast can prove

RoastMyOpsec does not score GPC, DNT, JSON-LD, Open Graph, or AI crawler groups as standalone Fs. The free roast may still surface third-party tracker-shaped hosts in HTML, plus security headers, TLS, cookies, path probes, and js-secrets. Honor GPC in your own consent stack; use the roast for origin hygiene, not as a privacy-law audit.

### FAQ

**Is DNT the same as GPC?**

No. DNT is a legacy Do Not Track header with weak adoption. GPC is a distinct opt-out signal many privacy regimes recognize.

**Does CSP replace honoring GPC?**

No. CSP restricts what the page may load. GPC is a preference about sale, sharing, and tracking behavior.

**Does RoastMyOpsec fail missing Sec-GPC handling?**

No. GPC is not a standalone F. Tracker-looking hosts in the document can still appear as tracker findings.

---

## GitHub Actions write-all vs Least-Privilege Permissions

- URL: https://roastmyopsec.com/blog/github-actions-write-all-vs-least-privilege
- Category: News
- Updated: 2023-12-26

GitHub Actions write-all vs least-privilege permissions is a CI identity decision, not a website header. permissions: write-all (or a default token that can push, contents, and more than the job needs) turns a compromised step into a repo-wide incident. Least privilege is an explicit permissions: block per workflow or job — often contents: read, plus only the extra scopes that job must have. Pinning third-party actions by full commit SHA stops a moving tag from swapping the script. RoastMyOpsec's optional GitHub deepen reads a sample of default-branch workflow YAML you authorize. It does not execute workflows, does not steal tokens, and a public URL roast cannot see .github at all.

### The practical difference

The website is the costume. Actions is the dressing room with a key to git. write-all is convenience. Least privilege is listing the rooms the job may enter. If a job only runs tests, it does not need contents: write.

Unpinned actions (uses: owner/action@v4) follow a tag the publisher can move. SHA pins freeze the exact commit you reviewed. This is the same class of decision as Subresource Integrity on a CDN script — mutability vs a digest.

### How to choose based on the workflow

Start every workflow with a tight default (contents: read, then add). Split jobs so deploy write is not on the same job that checks out untrusted pull-request code. Treat pull_request_target plus checkout of the PR head as a combination GitHub documents as dangerous — isolate untrusted code; do not give it the base-repo token.

Only deepen the repo that actually builds the site you roasted. Connecting a random org is outside the consent model.

| Pattern | What it grants | Typical miss | Takeaway |
| --- | --- | --- | --- |
| permissions: write-all | Broad GITHUB_TOKEN | One bad step writes the repo | Replace with an explicit map |
| contents: read (job default) | Clone for tests | Still too much if the job does not need the tree | Start here; add scopes per job |
| Action @v4 / @main | Whatever the tag points at tomorrow | Supply-chain swap | Pin full SHA; review upgrades |
| pull_request_target + untrusted checkout | Base-repo token with PR code | Untrusted code in a trusted identity | Do not combine; follow GitHub's isolation guidance |

### What deepen can and cannot prove

The paid GitHub check samples workflow files on the default branch for write-all, pull_request_target with checkout, and unpinned actions. It is a high-signal read, not a full CI pentest and not proof a workflow was exploited. File content of secrets is not dumped as a trophy.

If the leak is mixed content on www, tightening Actions will not rewrite the CMS. Run URL mode first.

### Common mistakes

The first mistake is a 10/10 URL roast and write-all on every workflow.

The second mistake is pinning nothing because 'we trust the marketplace.'

The third mistake is deepening a fork that never deploys production.

### FAQ

**Will a URL scan find GitHub Actions issues?**

No. Workflows are in the repo. Use optional GitHub deepen on a repository you own that ships that URL.

**Is write-all a vulnerability by itself?**

It is a privilege miss. It becomes an incident when a step is malicious or a dependency moves. Least privilege shrinks that blast radius.

**Does RoastMyOpsec run my workflows?**

No. It reads a bounded sample of YAML through the GitHub API after you authorize the repo. No exploit payloads, no token theft.

---

## fetchpriority vs loading=lazy for Public Sites

- URL: https://roastmyopsec.com/blog/fetchpriority-vs-loading-lazy
- Category: News
- Updated: 2023-11-22

fetchpriority vs loading=lazy for public sites is a competing-hints decision: loading=lazy defers offscreen images, while fetchpriority=high asks the browser to fetch a resource sooner — usually the LCP hero. Do not lazy-load the hero and do not mark every thumbnail high. RoastMyOpsec does not score these attributes as standalone Fs; they are performance hints, not browser security headers.

### The practical difference

loading=lazy is a native hint that below-the-fold images can wait. fetchpriority (HTML fetchpriority / Fetch Priority API) is a relative hint: high, low, or auto compared with other requests. They stack. A lazy image with fetchpriority=high is a contradiction. A hero with loading=lazy is a common Core Web Vitals own-goal.

Neither attribute sanitizes URLs. A lazy image still requested from a tracker CDN is still a tracker. fetchpriority=high on a third-party script is how you volunteer to run ads before content. CSP and first-party hosting remain the controls.

iframe loading=lazy exists too. Same rule: do not lazy the thing the user came to see; do lazy decorative embeds that sit below the fold.

### How to choose based on what you shipped

Set fetchpriority=high (and skip lazy) on the single LCP image. Leave loading=lazy on galleries and below-fold photos. Leave fetchpriority off for everything else. If a marketing pixel is in an img, removing it beats prioritizing it.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| fetchpriority=high | The one LCP image or a critical early fetch | Marking every asset high | Relative boost, not a lock |
| loading=lazy | Offscreen images and below-fold embeds | Lazy-loading the hero | Defer what the user cannot see yet |
| loading=eager (default) | Above-the-fold content you will paint immediately | Eager-loading a huge carousel | Fine for the first screen |
| Priority on third-party scripts | Almost never on a brochure page | Paying for trackers first | Host first-party; delay the rest |

### When loading=lazy still wins

loading=lazy still wins for long articles with many images the user may never scroll to. fetchpriority=high still wins for the single hero. Missing both attributes is normal. Contradicting them on the LCP image is a performance miss, not a roast F.

### Common mistakes

The first mistake is putting loading=lazy on the LCP hero.

The second mistake is setting fetchpriority=high on every thumbnail and tracker pixel.

The third mistake is treating missing fetchpriority as a security finding.

### What a URL roast can prove

The free roast does not grade fetchpriority, loading=lazy, preload, Open Graph, or JSON-LD. It does grade CSP, HSTS, framing, cookies, TLS, exposed .git/HEAD and .env, package.json signatures, and js-secrets. Image hints are speed knobs; tracker-shaped hosts in HTML may still show up as tracker findings.

### FAQ

**Should the hero image be lazy?**

No. Lazy-loading the LCP image delays the largest paint. Keep it eager and consider fetchpriority=high.

**Does fetchpriority replace CSP?**

No. It only hints scheduling. Script and image sources still follow CSP.

**Does RoastMyOpsec score loading=lazy?**

No. These attributes are not standalone Fs.

---

## Fenced Frames vs iframe sandbox for Ads Embeds

- URL: https://roastmyopsec.com/blog/fenced-frames-vs-iframe-sandbox
- Category: News
- Updated: 2023-11-04

Fenced frames vs iframe sandbox for ads embeds is two isolation costumes. iframe sandbox on a child you include limits scripts, forms, and same-origin access for that document. A fenced frame (<fencedframe>) is a Privacy Sandbox embed that is more opaque to the embedder — intended so an ad can run without the parent reading its content the way a normal iframe might. Neither stops someone else from framing you; that is frame-ancestors. A brochure with no ads can skip fenced frames and still sandbox any third-party iframe. RoastMyOpsec grades framing headers on the landing HTML. It does not parse iframe sandbox flags, does not score missing fenced frames as an F, and does not exploit clickjacking.

### The practical difference

sandbox is a flag you put on iframes you write. Fenced frames are a different element and API for participating ads flows, often paired with Protected Audience or similar. CSP frame-src names which hosts you may frame; it is not sandbox. Attribution Reporting measures conversions; it is not an embed tag.

Replacing every iframe with a fenced frame because 'cookies are dying' is not a security upgrade for a marketing site with a map widget.

### How to choose an embed

Third-party widget on a brochure: iframe sandbox without allow-same-origin unless you have a documented exception. Must not be framed yourself: frame-ancestors. Run ads in a Privacy Sandbox auction: fenced frames only after a vendor and privacy review — missing them is not a roast finding. Permissions-Policy can restrict unused APIs.

| Control | Who it isolates | Brochure default | Takeaway |
| --- | --- | --- | --- |
| iframe sandbox | A child you include | Usual widget win | See iframe sandbox vs frame-ancestors |
| Fenced frame | Opaque ads embed | Skip unless you run ads | Not a roast F if missing |
| frame-ancestors | Who may embed you | Set it | Clickjacking control |
| CSP sandbox | This document | Rarely for brochures | See CSP sandbox vs iframe sandbox |

### When fenced frames still win

They win when an ads stack you already use requires them and counsel signed off. iframe sandbox still wins for maps, video, and support chat. Missing fenced frames is healthy. Pair with third-party trackers if the embed is still a pixel plus a script.

### What the roast can prove

Framing headers can surface. Fenced-frame participation is not scored. iframe sandbox flags are not parsed. Confirm both yourself. Pair with consent-based scanning — do not roast someone else's ad iframe as research.

### Common mistakes

The first mistake is treating fenced frames as frame-ancestors.

The second mistake is requiring <fencedframe> as an OPSEC finding on a brochure.

The third mistake is allow-scripts plus allow-same-origin and calling the iframe sandboxed.

### FAQ

**Is missing fenced frames a vulnerability?**

No. It is an ads-embed choice. Missing it is not a roast F.

**Does a fenced frame replace iframe sandbox?**

No. Sandbox is for children you include. Fenced frames are a Privacy Sandbox ads surface. Login and widget iframes still use sandbox.

**Does RoastMyOpsec score fenced frames?**

No. It grades framing headers and does not parse iframe sandbox flags. No clickjacking payloads.

---

## FedCM vs Third-Party Cookies for Federated Login

- URL: https://roastmyopsec.com/blog/fedcm-vs-third-party-cookies
- Category: News
- Updated: 2023-10-27

FedCM vs third-party cookies for federated login is two ways a site can recognize a user who already has an account at an identity provider. Unpartitioned third-party cookies let an IdP iframe or redirect see a shared jar across sites — the old SSO and tracking model. Federated Credential Management (FedCM) is a browser API that shows a mediated account chooser so the relying party can get an identity assertion without that shared cookie. Neither is access control. A brochure with no Sign in can skip both. RoastMyOpsec does not click Sign in, does not call navigator.credentials, and does not score missing FedCM as an F. It may still grade cookie flags and login-path discoverability on the URL you paste.

### The practical difference

A third-party cookie is a silent jar. FedCM is an explicit prompt the browser owns. WebAuthn is a device-bound key for the relying party itself, not a federated IdP. Topics is ads, not login. Related Website Sets is a domain group, not an identity protocol.

Keeping unpartitioned IdP cookies 'until FedCM works everywhere' still ships the old tracking surface.

### How to choose a login plan

No login: skip FedCM and skip third-party cookies. First-party password or passkey on your origin: WebAuthn plus rate limits; no FedCM required. Must offer Google/Apple/GitHub: prefer FedCM or documented OAuth redirects over unpartitioned IdP cookies. Permissions-Policy can restrict identity-credentials-get if you do not participate.

| Channel | What it shares | Brochure default | Takeaway |
| --- | --- | --- | --- |
| Unpartitioned third-party cookie | Cross-site identity | Avoid | SSO and tracking cost |
| FedCM | Mediated account chooser | Skip unless you federate | Not a roast F if missing |
| WebAuthn passkeys | Device-bound first-party key | If you have a login | See WebAuthn vs passwords |
| OAuth redirect | Code on your origin | Usual federated win | Still HTTPS and CSRF-defend |

### When FedCM still wins

FedCM wins when you already federate login and a privacy review wants the browser in the middle. First-party passkeys still win for a site that owns the account. Missing FedCM is healthy. Pair with rate limits vs CAPTCHA if the login is still a password field.

### What the roast can prove

Cookie flags and known login paths can surface. FedCM participation is not scored. Confirm Identity Provider well-known files and Permissions-Policy yourself. Pair with consent-based scanning — do not roast someone else's IdP as research.

### Common mistakes

The first mistake is keeping unpartitioned IdP cookies as a 'fallback' after adding FedCM.

The second mistake is treating FedCM as a WAF or as MFA.

The third mistake is requiring FedCM as an OPSEC finding on a brochure.

### FAQ

**Is missing FedCM a vulnerability?**

No. It is a federated-login choice. Missing it is not a roast F.

**Does FedCM replace WebAuthn?**

No. FedCM is a mediated IdP chooser. WebAuthn is a first-party authenticator. Many sites want neither.

**Does RoastMyOpsec call the FedCM API?**

No. It does not run identity ceremonies. It may still grade cookies and login-path discoverability.

---

## ETag vs Cache-Control for Personalized Pages

- URL: https://roastmyopsec.com/blog/etag-vs-cache-control-for-personalized-pages
- Category: News
- Updated: 2023-08-28

ETag vs Cache-Control for personalized pages is a byte validator versus who may store the response. Cache-Control (private, no-store, no-cache) says whether a shared CDN may keep the HTML. An ETag is a validator for later If-None-Match revalidation. A user-specific page with Cache-Control: public and a stable ETag is how one person's dashboard can be served to the next. Cookies on the response without private/no-store is the same class. RoastMyOpsec inspects Cache-Control versus Set-Cookie on the landing HTML. It does not score ETag, does not send If-None-Match as a poisoning test, and does not brute-force validators.

### The practical difference

Cache-Control is policy: public, private, max-age, no-store. ETag is a name for a body version. Strong ETags mean byte-identical. Weak ETags (W/) mean semantically close. Neither header authenticates the user. Authorization still belongs in the app.

Static marketing assets want public plus ETag or hashed filenames. Account HTML wants no-store or private plus Vary: Cookie (and usually no shared ETag that ignores the session).

### How to choose validators

Public immutable assets: long max-age, filename hash, ETag optional. Personalized HTML: Cache-Control: private, no-store (or max-age=0, must-revalidate with private) and do not let the CDN cache by URL only. If you must revalidate, the ETag must not be identical across users for the same URL. See Cache-Control public vs private.

| Page type | Cache-Control | ETag role | Takeaway |
| --- | --- | --- | --- |
| Hashed JS/CSS | public, max-age long, immutable | Optional; filename already unique | Share freely |
| Marketing HTML | public with a short max-age, or CDN TTL | Fine if the body is the same for everyone | No Set-Cookie on that response |
| Logged-in HTML | private, no-store (or equivalent) | Must not be a shared user-agnostic tag | The miss if public + cookie |
| API JSON with cookies | private / no-store | Same rule | See CORS + cookies separately |

### What the roast can prove

Set-Cookie plus a public cache directive on the landing response can surface. A present ETag is not a finding. Confirm CDN keys include cookie or authorization where the body varies. Pair with HttpOnly cookies if the personalized page also sets a session.

### Common mistakes

The first mistake is CDN cache-everything on /dashboard.

The second mistake is a inode-based ETag that is identical for every user of the same template.

The third mistake is treating ETag as an access-control header.

### FAQ

**Is an ETag a vulnerability?**

Not by itself. A shared cache key plus a user-specific body is. Fix Cache-Control and the CDN key.

**Should I remove ETags everywhere?**

No. Static assets benefit. Personalized HTML should not be publicly cacheable whether or not an ETag exists.

**Does RoastMyOpsec score ETag?**

No. It compares Cache-Control to Set-Cookie on the landing response. No cache-poisoning tests.

---

## Defensive URL Audit vs Penetration Test

- URL: https://roastmyopsec.com/blog/defensive-url-audit-vs-penetration-test
- Category: News
- Updated: 2023-05-18

A defensive URL audit vs a penetration test is a scope and consent split. RoastMyOpsec is a consented self-audit of public HTTP, DNS, and optional GitHub for a URL you assert you own. It does not send exploit payloads, spray passwords, or hit private IPs. A penetration test is a contracted attempt to find and prove impact, often with authenticated sessions and a rules-of-engagement document. Use the roast to fix the costume. Hire a pentest when you need someone to try the doors you thought were locked.

### The practical difference

The audit gathers evidence from responses the internet can already fetch. Findings are headers, files, DNS, and scripts that showed up. The pentest is a project: scoping, accounts, maybe a staging clone, and a report that may include chained bugs.

Localhost and RFC1918 targets are blocked here on purpose. That is a safety rail, not a feature gap you should bypass.

### How to choose which you need this quarter

Ship or refresh a marketing site: start with a URL roast and the header/path punch list. Launch an app that holds money or health data: roast plus a real pentest and a bug-bounty later. Do not skip the cheap public-surface work because a pentest is on the calendar in November.

| Need | URL roast | Pentest | Takeaway |
| --- | --- | --- | --- |
| Missing CSP / HSTS | Yes | Overkill as the first tool | Fix the edge this week |
| Authz bugs between tenants | No | Yes | Needs accounts and a contract |
| .env on the CDN | Yes | They would find it too | Do not wait for the SOW |
| Private network / VPN app | Blocked | Scoped separately | Wrong tool; do not paste that URL |

### What 'no exploit payloads' means

Checks are reads, OPTIONS, bounded path GETs, and DNS lookups. Hostile Origin for CORS is a header, not a stolen session. GraphQL introspection is one read-only schema query. None of that is a proof-of-concept against your users.

If a vendor offers a 'scan' that fuzzes or dumps git objects recursively, that is a different product. Read their terms the way you read ours.

### Common mistakes

The first mistake is scanning a competitor and calling it research.

The second mistake is treating a 10/10 roast as a pentest letter for procurement.

The third mistake is pasting an internal hostname that should never be public.

### FAQ

**Is RoastMyOpsec hacking?**

No. It is a defensive, consent-based audit of public responses for a target you assert you control. See the Terms.

**Can a URL audit replace our annual pentest?**

No. It replaces the embarrassment of missing headers and exposed files before the pentesters arrive — not the pentest.

**What if the scanner cannot reach my site?**

That is scan status, not an OPSEC F. Confirm the URL is public, global DNS works, and the documented user agent is allowed.

---

## Dangling CNAME vs Abandoned SaaS Subdomains

- URL: https://roastmyopsec.com/blog/dangling-cname-vs-abandoned-saas-subdomain
- Category: News
- Updated: 2023-05-10

Dangling CNAME vs abandoned SaaS subdomains is the same failure seen from two sides. A dangling CNAME is DNS that still points at a provider host after you deleted the project. An abandoned SaaS subdomain is the empty project slot someone else can claim. Together they let a stranger serve content on a hostname customers still trust. RoastMyOpsec only raises this when a provider CNAME and a known unclaimed-resource fingerprint agree — it does not attempt to claim anything.

### The practical difference

The DNS record is your responsibility. The SaaS slot is the vendor's provisioning model. You must clean both: remove the CNAME, then delete or reclaim the vendor app.

Not every NXDOMAIN or placeholder page is a takeover. False positives are why evidence has to match a known fingerprint, not a vibe.

### How to choose a cleanup order

Inventory CNAMEs at the registrar or DNS host. Match each target to a live vendor project you still own. If the project is gone, remove the DNS first so the name stops resolving, then confirm the vendor dashboard.

| Symptom | Likely cause | First move | Takeaway |
| --- | --- | --- | --- |
| CNAME to vanished Heroku/GitHub/S3-style host | Project deleted, DNS left behind | Delete the CNAME | Do not wait for the vendor to expire it |
| Vendor still has the app, DNS moved | Orphaned project | Delete or lock the vendor app | Stop paying for ghosts, stop leaking names |
| Wildcard *.example.com to a dead target | Broad blast radius | Narrow the wildcard | Wildcards make cleanup harder |

### Why this is still showing up

Preview environments, old landing-page builders, and forgotten helpdesks are the usual leftovers. The marketing team launched on a vendor, the engineering team moved to a new host, and DNS was 'someone else's ticket.'

Treat decommission as a checklist: DNS, TLS certs, vendor project, analytics property, and any email CNAMEs.

### Common mistakes

The first mistake is pointing a brand subdomain at a vendor 'just to try it' and never deleting the record.

The second mistake is a wildcard CNAME for convenience.

The third mistake is assuming a 404 on the vendor host means nobody can register the slot.

### FAQ

**Is a dangling CNAME the same as subdomain takeover?**

A dangling CNAME is the DNS half. Takeover is what happens if the provider lets someone else claim the target. Fix DNS even if takeover is not currently possible.

**Will RoastMyOpsec try to take over my subdomain?**

No. The check is a public CNAME lookup plus known unclaimed-resource fingerprints. Nothing is registered or claimed.

**How often should I audit CNAMEs?**

Whenever you decommission a vendor, and on a quarterly DNS review for marketing subdomains.

---

## Content-Language vs HTML lang for Public Sites

- URL: https://roastmyopsec.com/blog/content-language-vs-html-lang
- Category: News
- Updated: 2022-08-10

Content-Language vs HTML lang for public sites is a header-versus-document decision: the lang attribute on <html> is what browsers, screen readers, and hyphenation use, while Content-Language is an HTTP metadata field for the intended audience language of the payload. Set lang (and xml:lang if you still ship XHTML) correctly on every document; Content-Language is optional extra that should match. RoastMyOpsec does not score either as a standalone F, and neither replaces hreflang for localized URL clusters.

### The practical difference

HTML lang is a BCP 47 tag on the document or on a subtree (a French quotation inside an English article). Assistive tech uses it to pick a voice. Spellcheck and translation heuristics use it. Content-Language, per RFC 9110, describes the language of the representation for processors that never parse HTML — including some caches and non-browser clients.

They can disagree. A CDN that stamps Content-Language: en on every response while the page is lang="de" is how you get English voices on German copy. hreflang is a third layer: it maps URLs to locales for search. Do not fake Content-Language as a substitute for real localized pages.

Language tags are not a privacy signal and not a security header. They do not hide a path. They do prevent embarrassing a11y misses.

### How to choose based on what you shipped

Always set <html lang> to the language of the page. Add Content-Language at the edge if non-HTML consumers or older proxies need it, matching the document. For mixed-language pages, lang on the root plus lang on the minority passages beats a confused header. Localized sites still need real URLs and hreflang, not headers alone.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| html lang | Every HTML document | Copy-pasting en on a translated page | This is the one to get right |
| Content-Language | You want HTTP metadata for non-HTML clients | CDN default that disagrees with the page | Optional; keep it honest |
| hreflang | You ship multiple locale URLs | Using it on a single-language site | Search cluster, not a11y |
| lang on a subtree | Quotations or widgets in another language | Forgetting to reset after the widget | Local override, not a header |

### When Content-Language still wins

Content-Language still wins on PDFs, plain text, and APIs that are not HTML but still have a human language. html lang still wins on documents. Missing Content-Language on a marketing HTML page is normal. Wrong html lang is the a11y miss.

### Common mistakes

The first mistake is leaving lang="en" on a translated template because the layout started in English.

The second mistake is letting a CDN stamp Content-Language that contradicts the document.

The third mistake is treating a missing Content-Language header as a security finding.

### What a URL roast can prove

RoastMyOpsec does not score Content-Language, html lang, hreflang, JSON-LD, or Open Graph as standalone Fs. The free roast is security headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets. Fix lang for humans and assistive tech; do not expect a URL roast to QA your locales.

### FAQ

**Is Content-Language a substitute for html lang?**

No. Browsers and accessibility trees read the lang attribute. Set that even if you also send the header.

**Does lang hide a page from crawlers?**

No. It describes language. Indexing uses robots, noindex, and canonicals.

**Does RoastMyOpsec fail missing lang?**

No. Document language is not a scored F on the free roast.

---

## Consent-Based URL Scans vs Unsolicited Probing

- URL: https://roastmyopsec.com/blog/consent-based-url-scan-vs-unsolicited-probing
- Category: News
- Updated: 2022-08-02

Consent-based URL scans vs unsolicited probing is the difference between a self-audit and someone else's problem. RoastMyOpsec is for a public HTTP URL you assert you control. Checks are reads, OPTIONS, bounded path GETs, and DNS — not exploit payloads, not password sprays, not private-network mapping. Localhost and RFC1918 targets are blocked. Pasting a competitor, a random government site, or a customer's staging host you do not own is outside the product. A security.txt Contact is how strangers report issues to you; it is not a voucher to run our scanner against them.

### The practical difference

Consent here means you are the operator (or have written authorization) for that origin. Unsolicited probing is scanning hosts because they are interesting. The roast cannot see your contract with a client; the Terms still require you to only submit targets you are allowed to audit.

A pentest SOW is a different consent artifact — scoped, often authenticated, sometimes with exploit proof. See defensive audit vs penetration test.

### How to choose what to paste

Your production marketing host and the app origin you run: yes. A vendor's SaaS login you merely use: no, unless they invite you. Internal hostnames that should never be public: do not paste them to a cloud scanner. If DNS is not global, the scan will fail closed — that is a feature.

| Target | Consent-based roast | Unsolicited probe | Takeaway |
| --- | --- | --- | --- |
| Your public www | Yes | N/A — you own it | This is the product |
| Competitor homepage | No | Don't | Not research; against Terms |
| 127.0.0.1 / 10.x | Blocked | Still wrong on our cloud | Wrong tool |
| Client site with a signed SOW | Only if they authorized this scanner | SOW ≠ our Terms | Get it in writing; still no exploits here |

### What 'blocked' and 'defensive' mean

The scanner refuses private IPs and localhost so you cannot use us as a pivot. Findings are headers, files, DNS, and scripts that already showed up on the public internet. Hostile Origin for CORS is a request header, not a stolen session.

If you need someone to break in, hire a pentest. If you need a punch list on a costume you own, roast it.

### Common mistakes

The first mistake is scanning a site because security.txt invited reports — that invites email, not our bot.

The second mistake is pasting a preview URL that is supposed to be secret.

The third mistake is treating a 10/10 roast as a license to scan the rest of the internet.

### FAQ

**Can I roast any public URL?**

Only URLs you are authorized to audit. Public reachability is not consent. See the Terms.

**Is RoastMyOpsec hacking?**

No. It is a defensive, consent-based read of public responses. No exploit payloads.

**Why was my internal hostname rejected?**

Private and loopback targets are blocked on purpose. Use a public origin you own, or a pentest scoped to that network — not this product.

---

## Client Hints vs User-Agent String Fingerprinting

- URL: https://roastmyopsec.com/blog/client-hints-vs-user-agent-fingerprinting
- Category: News
- Updated: 2022-07-16

Client Hints vs User-Agent string fingerprinting is how browsers donate high-entropy client data. The classic User-Agent header is a messy banner every request already sends. User-Agent Client Hints (Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, and high-entropy hints after Accept-CH / Critical-CH) are structured fields the site must request. Asking for model, bitness, or full version is a fingerprinting choice, not a performance freebie. Permissions-Policy can restrict ch-* features. RoastMyOpsec inspects Server and X-Powered-By on the landing response. It does not score Accept-CH, does not freeze a GREASE UA, and does not build a device fingerprint.

### The practical difference

UA strings shrink over time by design. Client Hints put the detailed bits behind an explicit request so random third parties do not get them by default. Your origin still gets whatever you Accept-CH. Third-party scripts on your page are a different privacy problem — see trackers vs first-party analytics.

Server banners (X-Powered-By) are origin disclosure. Client Hints are client disclosure. Strip the former; be stingy with the latter.

### How to choose what to request

Responsive CSS and a coarse UA or Sec-CH-UA-Mobile are enough for most marketing sites. Do not Accept-CH for model and full version unless a first-party feature truly needs it. Do not freeze a UA in logs as a session key. Critical-CH delays the first paint until hints return — use it only when the response must vary.

| Signal | Who sends it | Typical entropy | Takeaway |
| --- | --- | --- | --- |
| User-Agent | Every request (legacy) | Shrinking, still noisy | Do not treat as identity |
| Sec-CH-UA (low entropy) | After brands freeze / default | Browser brand, not the laptop | Usually enough |
| Accept-CH high-entropy | Only if you asked | Model, full version, bitness | Opt in only with a reason |
| Server / X-Powered-By | Your origin | Stack versions | Strip; see banners guide |

### What the roast can prove

Banner headers on the HTML response can surface as findings. Missing or present Accept-CH is not scored. Confirm Permissions-Policy for ch-* if you embed untrusted third parties. Pair with third-party trackers if a tag manager is the one collecting hints.

### Common mistakes

The first mistake is Accept-CH: * cargo-cult from a performance blog.

The second mistake is logging high-entropy hints next to emails in analytics.

The third mistake is treating Client Hints as a security header that hides the stack.

### FAQ

**Do Client Hints replace stripping Server headers?**

No. Hints describe the browser. Server and X-Powered-By describe you. Still strip banners.

**Should brochure sites send Accept-CH?**

Usually no, or only low-entropy fields you actually branch on. Missing Accept-CH is not a roast F.

**Does RoastMyOpsec score Client Hints?**

No. It reads origin banners. It does not request or freeze Client Hints.

---

## CDN and WAF Fingerprints vs Origin Exposure

- URL: https://roastmyopsec.com/blog/cdn-waf-fingerprints-vs-origin-exposure
- Category: News
- Updated: 2022-06-12

CDN and WAF fingerprints vs origin exposure is inventory, not a pass/fail CVE. Headers that name Cloudflare, Akamai, Fastly, or Vercel tell you traffic is probably not hitting the app VM first. Missing those fingerprints does not prove there is no WAF — some edges are quiet. A marketing origin that still answers on a raw IP or forgotten hostname is the miss: bots skip the WAF you paid for. RoastMyOpsec's vault edge check is passive header fingerprinting. It does not bypass WAFs or probe origin IPs.

### The practical difference

A fingerprint is a clue about the delivery path. cf-ray, x-cache, x-vercel-id, and similar strings are inventory. Origin exposure is a routing problem: DNS or a leftover A record still points at the application server.

Buying a WAF and leaving origin-example.com or an old IP in DNS is how the costume and the body live on different streets.

### How to choose what to fix first

Put public hostnames on the CDN. Restrict origin to the CDN's address space or authenticated pulls. Enable managed rules and rate limits on login and API routes. Do not treat a branded edge as a substitute for patches.

| Signal | Means | First move | Takeaway |
| --- | --- | --- | --- |
| Edge headers present | Traffic likely via a CDN/WAF | Turn on WAF rules and auth rate limits | Fingerprint is not configuration |
| No edge headers | Inconclusive | Check DNS and hosting panel | Quiet edges exist; naked origins also exist |
| Origin hostname still public | Bypass path | Lock DNS; firewall origin | This is the expensive miss |
| WAF in log-only forever | Costume | Enforce on auth routes first | Detection without blocking is theater |

### What still shows up this week

Teams put the marketing site on Vercel or Cloudflare and leave app.example.com on a VPS with the same cookies. Scan both hosts. Website mode and app mode exist for that split.

Pair with the admin-login and DNS hygiene guides: edge rules do not help a panel on an unlocked origin name.

### Common mistakes

The first mistake is celebrating a cf-ray header as 'we're secure.'

The second mistake is grey-clouding only www and leaving the apex on the origin.

The third mistake is WAF exceptions so wide that /api/* is allowlisted.

### FAQ

**Does a missing WAF fingerprint mean I have no WAF?**

No. Absence of a known header is not proof. Confirm in your DNS and CDN dashboard. Presence of a fingerprint is also not proof the rules are on.

**Should a marketing site always sit behind a CDN?**

Usually yes for DDoS, TLS, and header injection. The OPSEC win is keeping the origin off the public side streets.

**How does RoastMyOpsec detect an edge?**

Passive header fingerprints on the primary response. No bypass tests, no origin-IP scanning.

---

## CDN Cache Keys vs Cache-Control for Personalized Pages

- URL: https://roastmyopsec.com/blog/cdn-cache-key-vs-cache-control
- Category: News
- Updated: 2022-06-03

CDN cache keys vs Cache-Control for personalized pages is two different caches. Cache-Control tells browsers and shared caches whether HTTP may store the response (public, private, no-store). The CDN cache key is how the edge decides two requests are 'the same object' — often scheme, host, and path, sometimes ignoring query or cookies unless you say otherwise. If HTML varies by session cookie and the key does not include that cookie (or you marked it public), one profile's page can be stored as the anonymous URL. Vary: Cookie is the HTTP cousin; many CDNs still need an explicit cache-key rule. RoastMyOpsec compares Cache-Control to Set-Cookie on the landing HTML. It does not read CDN dashboards, does not purge keys, and does not score missing cache-key customizations.

### The practical difference

Cache-Control is the origin's freshness policy. The cache key is the index. ETag is a validator, not a key. Service workers are a third cache on the device. See Cache-Control public vs private, ETag vs Cache-Control, and service workers vs Cache-Control.

A brochure with no cookies can use a boring path-only key and public caching. An account app cannot.

### How to choose a keying plan

Static hashed assets: path-only key, long TTL, Cache-Control public. Personalized HTML: Cache-Control private, no-store, and do not cache at the CDN — or key on the session cookie you actually vary on, which is easy to get wrong. Prefer not caching logged-in HTML at the edge at all. Bypass cache when Set-Cookie is present unless the page is a logged-out shell.

| Layer | What it decides | Personalized HTML | Takeaway |
| --- | --- | --- | --- |
| Cache-Control | May HTTP store this? | private, no-store | Origin policy |
| CDN cache key | Which requests share a slot | Usually do not cache | Not a roast score; still hygiene |
| Vary | Which request headers change the representation | Use carefully with cookies | HTTP cousin of a key |
| Service worker | Device Cache Storage | Usually skip session bodies | Different cache |

### When a custom cache key still wins

It wins for A/B cookies on otherwise public marketing HTML when you know the cookie is not a session. It never wins as a substitute for no-store on /dashboard. Missing a custom key on a brochure is healthy.

### What the roast can prove

Set-Cookie plus public Cache-Control on the landing response can surface. Cache-key rules are out of scope. Confirm in the CDN dashboard. Pair with ETag if a user-agnostic validator sits on a personalized URL.

### Common mistakes

The first mistake is caching / as public while a welcome-back banner is cookie-shaped.

The second mistake is including every cookie in the key and fragmenting the cache into garbage.

The third mistake is treating a roast miss on Cache-Control as proof the CDN key is safe.

### FAQ

**Does Cache-Control private stop the CDN from caching?**

It should, if the CDN honors it. Confirm the product. Do not rely on a custom key as the only lock.

**Should brochure sites customize cache keys?**

Usually no. Path-only plus public caching for static assets is enough. Missing a custom key is not a roast F.

**Does RoastMyOpsec read CDN cache keys?**

No. It compares Cache-Control to Set-Cookie on a consented GET. Confirm keys in your CDN dashboard.

---

## Breadcrumb JSON-LD vs Visible Nav for Public Sites

- URL: https://roastmyopsec.com/blog/breadcrumb-jsonld-vs-visible-nav
- Category: News
- Updated: 2022-04-30

Breadcrumb JSON-LD vs visible nav for public sites is a markup-versus-UI decision: BreadcrumbList should repeat a trail users can actually see and click, while primary nav is the site-wide menu and does not have to be a breadcrumb. Do not invent a JSON-LD trail that lists internal or unpublished sections. RoastMyOpsec does not score BreadcrumbList as a standalone F, and fake crumbs are a public map if you fill them with sensitive prefixes.

### The practical difference

Google’s breadcrumb structured data is a BreadcrumbList of ListItems with position, name, and item URL. It is meant to match a visible breadcrumb. A header nav with Products / Docs / Blog is not automatically a breadcrumb. Using JSON-LD to describe a trail that is not on the page is how rich results disagree with reality.

The OPSEC miss is encoding a trail like Home > Internal > Staging that never appears in the UI. That is a sitemap-quality leak in Schema.org clothing. Visible nav that links only to public sections is ordinary. JSON-LD that adds extra unpublished crumbs is not.

Breadcrumbs are not access control. A 200 on each item URL is still a 200.

### How to choose based on what you shipped

If the template already shows breadcrumbs, mirror them in JSON-LD (or Microdata) one-for-one. If you only have a global nav, skip BreadcrumbList. Never add crumbs “for SEO” that point at URLs you would not put in the footer.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Visible breadcrumbs + JSON-LD | The page already shows a matching trail | Drift between UI and graph | Mirror, do not invent |
| Visible nav only | A flat marketing site with no trail UI | Forcing BreadcrumbList anyway | Skip the type |
| JSON-LD without UI | Almost never | Advertising URLs users cannot see | Do not ship a ghost trail |
| Sitemap instead | You want crawlers to find public sections | Listing URLs that should stay unpublished | Different job than crumbs |

### When breadcrumb JSON-LD still wins

Breadcrumb JSON-LD still wins when the visible trail is real and you want search to show it. Visible nav still wins as the actual way humans move. Missing BreadcrumbList on a homepage is normal. A graph that invents extra sections is the miss.

### Common mistakes

The first mistake is adding BreadcrumbList items that do not appear in the page UI.

The second mistake is putting unpublished or internal section URLs in the trail graph.

The third mistake is treating missing breadcrumb JSON-LD as a security finding.

### What a URL roast can prove

RoastMyOpsec does not grade BreadcrumbList, other JSON-LD, Open Graph, or hreflang as standalone Fs. It does roast chatty robots.txt Disallow lines and exposed paths such as .git/HEAD and .env, plus headers, TLS, cookies, and js-secrets. Do not use structured crumbs as a second sitemap of sensitive prefixes.

### FAQ

**Must breadcrumbs be in JSON-LD?**

Only if you want that rich result and the trail is visible. A nav menu alone does not require BreadcrumbList.

**Can JSON-LD breadcrumbs hide a URL?**

No. They advertise URLs. If a URL should not be public, do not list it and do not serve it.

**Does RoastMyOpsec fail missing breadcrumbs?**

No. Structured trails are not a scored F.

---

## BIMI vs DMARC for Brand Logos in the Inbox

- URL: https://roastmyopsec.com/blog/bimi-vs-dmarc-for-brand-email
- Category: News
- Updated: 2022-04-22

BIMI vs DMARC for brand logos in the inbox is a display feature versus the authentication policy it sits on. DMARC (with SPF and DKIM) tells receivers what to do when From alignment fails. BIMI (Brand Indicators for Message Identification) is a DNS TXT on default._bimi that points at an SVG mark — and, for many providers, a verified mark certificate — so supporting inboxes can show your logo. Without DMARC at enforcement, BIMI is a wish. It does not replace MTA-STS, website HSTS, or a WAF. RoastMyOpsec is a consented GET to a URL. It does not query BIMI TXT, does not fetch SVG marks, and does not score missing BIMI as an F.

### The practical difference

DMARC is the spoofing control. BIMI is branding on mail that already passed. MTA-STS is SMTP TLS to your MX. The marketing homepage TLS check does not see any of this. See SPF vs DKIM vs DMARC and MTA-STS vs DMARC.

A BIMI SVG that embeds scripts or unexpected hosts is the wrong file type for this job. Keep the mark boring, tiny, and on a host you control.

### How to choose a policy

Every sending domain: SPF, DKIM, DMARC with a rua you read, then p=quarantine or reject when flows are clean. BIMI: only after enforcement, with an SVG that matches the trademark you can prove. Skip BIMI on a domain that barely sends mail. Do not publish a BIMI record that points at a dangling CNAME.

| Control | What it does | Depends on | Takeaway |
| --- | --- | --- | --- |
| DMARC | From alignment disposition | SPF / DKIM | Do this first |
| BIMI | Logo in some inboxes | DMARC enforcement + mark | Optional branding; not a roast F |
| MTA-STS | SMTP TLS to MX | Healthy policy host | Transport, not a logo |
| Website HTTPS | Browser to origin | Cert + HSTS | Different layer |

### When BIMI still wins

BIMI wins as a brand cue after DMARC is real. DMARC still wins as the security control. Missing BIMI on a brochure domain is healthy. Pair with DNS hygiene if the BIMI hostname is leftover SaaS.

### What the roast can prove

Nothing about BIMI or DMARC. Confirm records in your mail console. Pair with security.txt if you also want a researcher inbox on the web origin.

### Common mistakes

The first mistake is a BIMI record before DMARC p=quarantine or reject.

The second mistake is hosting the SVG on a bucket that also lists .env.

The third mistake is treating a missing BIMI logo as an OPSEC finding.

### FAQ

**Does BIMI replace DMARC?**

No. BIMI is a logo on mail that already authenticates. DMARC is the policy.

**Should every marketing domain publish BIMI?**

Only if you send mail, enforce DMARC, and care about inbox logos. Missing BIMI is not a roast F.

**Does RoastMyOpsec check BIMI?**

No. It does not query DNS TXT or fetch BIMI SVG files.

---

## Attribution Reporting vs Topics API for Marketing Sites

- URL: https://roastmyopsec.com/blog/attribution-reporting-vs-topics-api
- Category: News
- Updated: 2022-03-27

Attribution Reporting vs Topics API for marketing sites is two Privacy Sandbox ads channels. Topics lets participating sites observe coarse interest topics the browser computed. Attribution Reporting (ARA) lets an advertiser and a publisher measure conversions with delayed, aggregated, or event-level reports — not a durable third-party cookie, still a measurement pipeline. Neither is access control. A brochure that needs pageviews can stay on first-party analytics and skip both. Permissions-Policy can restrict attribution-reporting and browsing-topics. RoastMyOpsec pattern-matches common analytics and marketing hosts in public HTML. It does not score ARA, does not register sources or triggers, and does not treat missing Attribution Reporting as an F.

### The practical difference

Topics is 'what this browser might be into.' Attribution Reporting is 'did an ad on site A relate to a conversion on site B,' with browser-enforced limits. Private State Tokens are anti-fraud signals. Third-party cookies are the old shared jar. First-party analytics is your origin's counts.

Adding an ARA snippet because 'cookies are dying' still adds a third-party script unless you use a first-party wrapper you actually control.

### How to choose a measurement plan

Need counts on your own site: first-party analytics, tight cookies, no None on the session. Need ads conversion measurement: talk to counsel and vendors; do not keep unpartitioned trackers 'until ARA works.' Use Permissions-Policy to disable unused attribution and Topics if you do not participate. ads.txt is authorized sellers, not a measurement toggle.

| Channel | What it measures | Brochure default | Takeaway |
| --- | --- | --- | --- |
| Topics API | Coarse interests | Skip unless you sell ads | See Topics vs cookies |
| Attribution Reporting | Ad conversions, delayed | Skip unless you run ads | Not a roast F if missing |
| First-party analytics | Your origin's counts | Usual win | See trackers vs first-party |
| Unpartitioned third-party cookie | Cross-site identity | Avoid | High tracking cost |

### When Attribution Reporting still wins

ARA wins as a vendor requirement on a site that already runs ads and has a privacy review. First-party analytics still wins for a brochure. Missing ARA is healthy. Pair with ads.txt if you actually sell inventory.

### What the roast can prove

Known tracker hosts in HTML can surface. Attribution Reporting participation is not scored. Confirm Permissions-Policy and scripts yourself. Pair with consent-based scanning — do not roast third-party ad hosts as research.

### Common mistakes

The first mistake is keeping unpartitioned ad cookies as a backup after adding ARA.

The second mistake is treating Attribution Reporting as a security header.

The third mistake is requiring ARA as an OPSEC finding.

### FAQ

**Is missing Attribution Reporting a vulnerability?**

No. It is an ads measurement choice. Missing it is not a roast F.

**Does ARA replace first-party analytics?**

No. ARA is conversion measurement for participating ad flows. Pageviews still belong first-party if you need them.

**Does RoastMyOpsec register attribution sources?**

No. It may pattern-match known marketing hosts in HTML. It does not observe conversions.

---

## assetlinks.json vs apple-app-site-association

- URL: https://roastmyopsec.com/blog/assetlinks-vs-apple-app-site-association
- Category: News
- Updated: 2022-03-19

assetlinks.json vs apple-app-site-association is Android App Links versus Apple Universal Links, both published under /.well-known/. Google looks at https://example.com/.well-known/assetlinks.json. Apple looks at apple-app-site-association (often without a file extension) on the HTTPS origin that matches the applinks domain. Both files are meant to be public: package names, SHA-256 cert fingerprints, team IDs, and path prefixes. They are not API keys. Do not put signing secrets, internal hostnames, or debug bundle IDs you would not print on the marketing site. Path statements should not open authenticated deep links that skip login — that is an open-redirect-class product bug, not a roast finding. RoastMyOpsec does not require these files, does not score them, and does not verify app signatures. Missing them is an app-link gap, not an OPSEC F.

### The practical difference

assetlinks.json is a JSON statement list: this Android package, this cert fingerprint, may handle these relations (typically delegate_permission/common.handle_all_urls). AASA is Apple's JSON: applinks details with appIDs and paths or components. Serve both over HTTPS with a boring Content-Type. Do not hide them behind a login.

security.txt and change-password are other well-known files with different jobs. Do not merge them.

### How to choose what to publish

Production apps only. Staging package names belong on staging hosts, not on the brand apex. Paths: public content you actually want to open in-app. Exclude /reset?token= style URLs from associated paths so a mail client does not hand a token to the wrong surface — see tokens in URLs.

| File | Platform | Typical contents | Takeaway |
| --- | --- | --- | --- |
| /.well-known/assetlinks.json | Android App Links | Package + cert fingerprints | Public by design; no secrets |
| /.well-known/apple-app-site-association | iOS Universal Links | Team ID + bundle + paths | HTTPS only; no auth wall |
| /.well-known/security.txt | Researchers | Contact, policy | Different well-known |
| /.well-known/change-password | Password managers | Redirect to the form | Different well-known |

### What the roast can prove

A 404 on either file is not a finding. A 200 that is HTML from a SPA catch-all is a product bug for App Links — browsers and OS fetchers need JSON, not the homepage. Confirm Content-Type yourself. The roast does not parse AASA.

### Common mistakes

The first mistake is serving AASA only on www while applinks lists the apex.

The second mistake is leaving a debug team ID in production AASA.

The third mistake is treating a missing assetlinks.json like a missing CSP.

### FAQ

**Are Digital Asset Links a vulnerability if public?**

No. The file is supposed to be public. Secrets and leftover debug apps in it are the miss.

**Can I robots.txt Disallow these files?**

OS fetchers still need them. Disallow does not hide them from a GET. Do not use robots.txt as access control.

**Does RoastMyOpsec check App Links files?**

No. They are not scored. Missing them is not an OPSEC F.

---

## AMP vs Signed HTTP Exchanges for Public Pages

- URL: https://roastmyopsec.com/blog/amp-vs-signed-http-exchanges
- Category: News
- Updated: 2022-03-02

AMP vs Signed HTTP Exchanges for public pages is two ways a third party might serve a copy of your HTML. AMP is a constrained HTML dialect historically cached on a Google origin (cdn.ampproject.org and cousins) — extra CSP, extra origin, extra supply chain. Signed HTTP Exchanges (SXG) let a cache serve a cryptographically signed snapshot that still attributes to your origin in supporting browsers. Neither is HTTPS, HSTS, or a WAF. A brochure can stay on first-party HTML at the CDN you already pay for. RoastMyOpsec does not score AMP, does not fetch SXG certificates, and does not treat missing AMP as an F.

### The practical difference

AMP is a format plus a cache network. SXG is a signature on an HTTP exchange so a prefetch cache can show your origin's URL. HTTP/3 is transport. Cache-Control is freshness. Alt-Svc advertises h3 — not an AMP cache.

Keeping AMP 'for SEO' in 2026 still adds a second HTML pipeline and a second origin unless you self-host AMP without the Google cache.

### How to choose a packaging plan

Brochure: first-party HTML, Cache-Control, HTTP/2 and HTTP/3 on your hostname. Must stay in a partner prefetch program: SXG only after cert and cache-key review — still not a roast finding if missing. Still on AMP: prefer first-party AMP or migrate off; do not treat the Google cache as your WAF. Personalized HTML: do not SXG-sign Set-Cookie pages.

| Channel | Who serves the HTML | Brochure default | Takeaway |
| --- | --- | --- | --- |
| First-party CDN | Your hostname | Usual win | See HTTP/2 vs HTTP/3 |
| AMP cache | A third-party AMP origin | Skip unless required | Extra origin and CSP |
| Signed HTTP Exchange | A cache with your signature | Skip unless you prefetch | Not a roast F if missing |
| Cache-Control | Freshness of your response | Always set it | See CDN cache keys |

### When AMP or SXG still wins

AMP wins only when a partner still requires the dialect. SXG wins when a documented prefetch program needs signed snapshots of public, non-personalized pages. First-party HTML still wins for a brochure. Missing both is healthy.

### What the roast can prove

HTTPS and Cache-Control vs cookies can surface. AMP and SXG are not scored. Confirm amphtml links and SXG certs yourself. Pair with consent-based scanning — do not treat a Google cache URL as a site you own.

### Common mistakes

The first mistake is treating the AMP cache as your origin for cookies and CSP.

The second mistake is signing personalized HTML as an SXG.

The third mistake is requiring AMP or SXG as an OPSEC finding.

### FAQ

**Is missing AMP a vulnerability?**

No. It is a packaging choice. Missing AMP is not a roast F.

**Does SXG replace HTTPS?**

No. The exchange is still HTTP over TLS at the cache. SXG adds a signature so a prefetch can show your origin URL.

**Does RoastMyOpsec score AMP or SXG?**

No. It does not fetch AMP caches or SXG certificates. Confirm those pipelines yourself.

---

## Alt-Svc vs Origin IP Exposure on CDNs

- URL: https://roastmyopsec.com/blog/alt-svc-vs-origin-ip-exposure
- Category: News
- Updated: 2022-02-21

Alt-Svc vs origin IP exposure on CDNs is a performance header versus a bypass map. Alt-Svc (RFC 7838) tells supporting browsers they may use another protocol or host for this origin — commonly h3=":443" for HTTP/3 on the same name. That is not a security control. Putting a raw origin IP, a staging hostname, or a non-CDN VIP in Alt-Svc is how you invite clients around the WAF. Server banners and CDN fingerprints are a different disclosure class. RoastMyOpsec inspects public landing headers for banners and does not score Alt-Svc, does not speak HTTP/3 as a finding, and does not probe origin IPs.

### The practical difference

Same-host h3 on 443 is a protocol upgrade. A different host in Alt-Svc is a new name you must TLS-authenticate as this origin (the spec is strict). An IP in the advertisement is an inventory gift. Keep origin IPs off DNS, off error pages, and off Alt-Svc.

www vs apex still needs one canonical host. Alt-Svc should not resurrect the loser hostname as a secret HTTP/3 endpoint.

### How to choose an Alt-Svc

If the CDN terminates HTTP/3 on the same public name, a boring h3=":443" (with a sane ma=) is fine. If you do not offer HTTP/3, omit the header. Do not advertise a second company domain you do not control. Do not copy an origin's Alt-Svc through the CDN unchanged if the origin named an internal host.

| Advertisement | Typical intent | OPSEC risk | Takeaway |
| --- | --- | --- | --- |
| h3=":443" same host | HTTP/3 on the edge name | Low if the name is the public CDN | Performance; not a WAF |
| Alt-Svc to an origin IP | Usually a misconfig | Bypass map | Remove it |
| Alt-Svc to staging.example | Wrong environment | Inventory + weaker auth | Keep env split |
| Server / via / cf-ray | CDN fingerprint | Stack hint, not an IP | See CDN vs origin guide |

### What the roast can prove

Banner and CDN-shaped headers on the HTML response can surface. Alt-Svc is not scored. Confirm the header in your CDN dashboard. Pair with dangling CNAMEs if a leftover name still resolves.

### Common mistakes

The first mistake is origin Alt-Svc leaking through to the browser.

The second mistake is advertising HTTP/3 on a host whose cert SAN does not match.

The third mistake is treating missing Alt-Svc as an OPSEC F.

### FAQ

**Is Alt-Svc a security header?**

No. It advertises alternative services. Keep it on your public CDN name only.

**Should every site enable HTTP/3 Alt-Svc?**

Only if the edge actually speaks it. Missing Alt-Svc is not a roast finding.

**Does RoastMyOpsec score Alt-Svc?**

No. It does not treat HTTP/3 as a vulnerability and does not probe origin IPs.

---

## AI Crawlers vs Search Crawlers in robots.txt for Public Sites

- URL: https://roastmyopsec.com/blog/ai-crawlers-vs-search-crawlers-robots
- Category: News
- Updated: 2022-02-13

AI crawlers vs search crawlers in robots.txt for public sites is a User-agent grouping decision: keep Googlebot and other search crawlers on the policy you want for search, and add separate groups for named AI crawlers only if you have a real product reason to treat them differently. robots.txt is still not access control. RoastMyOpsec reads Disallow lines for treasure-map shapes; it does not score AI crawler groups, llms.txt, or IndexNow as standalone Fs.

### The practical difference

RFC 9309 is the Robots Exclusion Protocol. Well-behaved crawlers pick the most specific User-agent group. Search crawlers exist to index public pages you want found. AI training or retrieval crawlers use different names; vendors publish those names and they change. A blanket Disallow for one name does not bind a crawler that ignores robots.txt.

llms.txt is an optional brochure of URLs you are willing to summarize. It is not a robots group. Splitting search and AI in robots.txt is a content policy. Listing backup or admin-shaped prefixes so “the model does not see them” is still a public map for everyone else.

Google publishes crawler overview docs so you can distinguish Googlebot from other Google user agents. Copy-pasting a viral “block all AI” snippet that also hits your search crawler is how the blog disappears from Google.

### How to choose based on what you shipped

Want search: allow your public articles to search crawlers, keep a sitemap of those URLs, keep Disallow boring. Want to discourage some AI crawlers: add named groups without touching the search groups, and do not put secrets in either file. Want AI summaries of docs: a calm llms.txt of public docs, not a second Disallow novel.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Search crawler groups | You want ordinary search indexing | A viral snippet that blocks Googlebot too | Keep search policy separate |
| Named AI crawler groups | You have a reason to treat those agents differently | Names change; non-compliant crawlers ignore you | Policy, not a vault |
| llms.txt | You want to point models at public docs | Copying Disallow paths into the brochure | Optional; not scored |
| Auth / edge deny | The resource should not be fetched by anyone | Relying on User-agent strings | The actual control |

### When AI-crawler rules still win

Named AI groups still win when legal or product wants a different crawl policy than search and you will maintain the names. Search groups still win for discoverability. A missing AI group is normal. A Disallow list of sensitive prefixes is the roastable miss either way.

### Common mistakes

The first mistake is pasting a block-all-AI robots.txt that also stops your search crawler.

The second mistake is Disallowing sensitive prefixes to “hide them from GPT” in a public file.

The third mistake is treating a missing AI User-agent group as a security finding.

### What a URL roast can prove

On a consented scan, RoastMyOpsec fetches /robots.txt and may roast Disallow lines that look like a sensitive-area tour. It does not score AI versus search grouping, llms.txt, IndexNow, GPC, or rel=me as standalone Fs. The free pack still covers headers, TLS, cookies, .git/HEAD, .env, package.json signatures, and js-secrets. User-agent rules are not a lock.

### FAQ

**Does robots.txt stop all AI crawlers?**

Only the well-behaved ones that honor RFC 9309 and match your User-agent groups. It is not access control.

**Should I put admin paths in a GPTBot Disallow?**

No. Do not advertise sensitive prefixes in robots.txt. Deny or do not deploy those URLs.

**Does RoastMyOpsec grade AI crawler rules?**

No. It may comment on juicy Disallow lists. Missing AI groups are not an F.

---

## ads.txt vs app-ads.txt on Public Marketing Origins

- URL: https://roastmyopsec.com/blog/ads-txt-vs-app-ads-txt
- Category: News
- Updated: 2022-02-04

ads.txt vs app-ads.txt on public marketing origins is two IAB Tech Lab files that list who may sell your ad inventory. ads.txt lives at https://example.com/ads.txt for the website. app-ads.txt lives on the developer domain you declare in the app store, often /app-ads.txt, for in-app ads. Both are supposed to be public and fetchable without cookies. They are not a WAF, not security.txt, and not access control. A missing file is normal if you do not sell ads. A file that pastes origin IPs, private reseller passwords, or a CMS version résumé is the miss. RoastMyOpsec does not score ads.txt or app-ads.txt and does not verify ad-tech authorization.

### The practical difference

ads.txt authorizes sellers for the web origin. app-ads.txt authorizes sellers for the mobile app, published on a domain you control. The web app manifest and assetlinks.json are other public catalogs with different jobs. security.txt is researcher contact — do not put Contact only inside ads.txt.

Third-party ad tags are a script blast radius. The text file does not replace CSP, SRI, or first-party analytics. See trackers vs first-party analytics.

### How to choose what to publish

You sell display ads on this host: keep ads.txt accurate, HTTPS, no redirects that break crawlers, no secrets. You ship an app with ads: publish app-ads.txt on the developer domain the store listing names. You sell nothing: omit both; a 404 is healthy. Do not copy a competitor's file. Do not list internal SSP hostnames that skip the CDN.

| File | Job | Public origin | Takeaway |
| --- | --- | --- | --- |
| /ads.txt | Web authorized sellers | Yes, if you sell ads | Inventory list, not a lock |
| /app-ads.txt | App authorized sellers | On the developer domain | Same class; different product |
| security.txt | Vulnerability contact | If you will read reports | Different well-known job |
| Web app manifest | PWA metadata | If you install | See manifest vs public metadata |

### When ads.txt still wins

It wins when you actually sell ads and want buyers to check authorization. It never wins as a substitute for header hygiene. Missing ads.txt on a brochure site is not a roast F.

### What the roast can prove

Neither file is scored. Tracker-shaped hosts in HTML can still surface. Confirm ads.txt in your own browser. Pair with humans.txt if the same origin over-shares staff names in another text file.

### Common mistakes

The first mistake is putting a shared ad-network password in a comment at the bottom of ads.txt.

The second mistake is treating a 200 on ads.txt as proof the tag manager is safe.

The third mistake is requiring ads.txt on a site that does not sell ads.

### FAQ

**Is a missing ads.txt a vulnerability?**

No. If you do not sell ads, omit the file. If you do sell ads, a missing or stale file is a business/ads-txt compliance miss, not a roast F.

**Can ads.txt replace security.txt?**

No. Different parsers, different jobs. Researcher contact belongs in RFC 9116.

**Does RoastMyOpsec fetch ads.txt?**

Not as a scored path. It may still pattern-match known ad and analytics hosts in HTML.

---

## Access-Control-Max-Age vs CORS Preflight Caching

- URL: https://roastmyopsec.com/blog/access-control-max-age-vs-cors-preflight
- Category: News
- Updated: 2022-01-18

Access-Control-Max-Age vs CORS preflight caching is how long a browser may skip repeating an OPTIONS check, not whether the API is safe. Access-Control-Max-Age is a seconds budget on a successful preflight. A large value cuts OPTIONS traffic; it also delays browsers from seeing a tighter allowlist after you ship a fix. Wildcard origins, credentialed echoes, and missing methods stay wrong no matter the cache. Browsers cap the value — Chrome has historically clamped around two hours. RoastMyOpsec app mode sends a bounded untrusted Origin and reads ACAO and credentials. It does not score Max-Age, does not send your users' cookies, and does not attempt to skip or forge preflights.

### The practical difference

Preflight is the browser asking 'may this cross-origin request use these methods and headers?' Max-Age is the answer's TTL. Access-Control-Allow-Origin and Allow-Credentials are the policy. Mixing them up is how teams 'optimize CORS' and leave * with cookies.

Simple GETs without custom headers may skip preflight entirely. Max-Age never runs for those. Policy still matters.

### How to choose a Max-Age

If the allowlist is stable and public, a moderate Max-Age (minutes to the browser cap) is fine. If you are rolling back a bad ACAO, expect cached preflights until TTL or a browser restart. Do not use Max-Age as a WAF. Do not set it to years and assume clients obey.

| Knob | What it changes | What it does not | Takeaway |
| --- | --- | --- | --- |
| Access-Control-Max-Age | How long OPTIONS may be reused | Who is allowed | Cache, not an allowlist |
| Access-Control-Allow-Origin | Which origin may read the response | Server authentication | See wildcard vs allowlist |
| Access-Control-Allow-Credentials | Whether cookies may be included | Whether * is legal | Cannot pair with * |
| Timing-Allow-Origin | Resource Timing visibility | CORS access to the body | Different header; see TAO |

### What the roast can prove

A roast can show ACAO reflecting an untrusted Origin and credentials behavior. Absence of Max-Age is not a finding. A huge Max-Age on a public API is an ops note, not an F. Pair with CORS credentials vs wildcard if cookies are in play.

### Common mistakes

The first mistake is setting Max-Age to 86400 and thinking Chrome will honor a full day.

The second mistake is tightening ACAO and wondering why old SPAs still succeed until the preflight cache dies.

The third mistake is treating a missing Max-Age like a missing CSP.

### FAQ

**Does a high Access-Control-Max-Age weaken CORS?**

It slows policy changes in browsers that already cached a preflight. It does not grant new origins. Fix Allow-Origin first.

**Should every API set Max-Age?**

Only if preflights are hot and the allowlist is stable. Public static assets that never preflight do not need it.

**Does RoastMyOpsec score Access-Control-Max-Age?**

No. The scan reads ACAO and credentials from a bounded Origin. Max-Age is not a scored finding.


# Safety

## X-XSS-Protection vs CSP for XSS Defense

- URL: https://roastmyopsec.com/blog/x-xss-protection-vs-csp
- Category: Safety
- Updated: 2026-08-22

X-XSS-Protection vs CSP is not a close race. X-XSS-Protection enabled the old IE/Chrome XSS auditor. That filter is retired, inconsistent, and has caused bypass-and-worse stories. Content-Security-Policy (enforcing, with nonces or hashes — not unsafe-inline forever) is the current browser control for unexpected scripts. Marketing sites should ship CSP and may set X-XSS-Protection: 0 so leftover auditors stay off. Do not add 1; mode=block as a 'security header win.' RoastMyOpsec grades CSP quality on the primary HTML response. It does not run XSS payloads and it does not treat a missing auditor header as a failure.

### The practical difference

The auditor tried to guess reflected XSS in the response. Guessing is not a policy. CSP lists what may run. Trusted Types (later) lists how the DOM may be written. HttpOnly cookies reduce theft of the session if XSS still happens.

Some CDNs still emit X-XSS-Protection: 1; mode=block from a 2016 template. Delete it or set 0.

### How to choose what to send

Enforce CSP (frame-ancestors now, script-src as soon as tags allow). Optional: X-XSS-Protection: 0. Do not spend a sprint tuning the auditor. Pair with report-only vs enforcing and nonces vs hashes.

| Header | Status | What to do | Takeaway |
| --- | --- | --- | --- |
| CSP enforcing | Current | Nonce/hash; no forever unsafe-inline | This is the control |
| X-XSS-Protection: 0 | Disables leftover auditors | Fine if a template insists on the header | Better than 1; mode=block |
| X-XSS-Protection: 1; mode=block | Obsolete / risky | Remove | Not a CSP substitute |
| Missing both | No XSS policy | Add CSP | Auditor would not have saved you anyway |

### What the roast can see

CSP looseness and presence. An auditor header is not a scoring trophy. A 10/10 header pack still is not a pentest — see defensive audit vs penetration test.

### Common mistakes

The first mistake is a checklist that still requires XSS-Protection 1.

The second mistake is mode=block plus unsafe-inline CSP.

The third mistake is disabling CSP because 'we have the auditor.'

### FAQ

**Should I enable X-XSS-Protection?**

No. Prefer CSP. If the header must exist, send 0. Do not use 1; mode=block as your XSS plan.

**Does RoastMyOpsec fail sites without X-XSS-Protection?**

No. The free header pack cares about CSP, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy.

**Is XSS-Protection the same as Trusted Types?**

No. Trusted Types is a modern DOM-sink policy. The auditor is a retired heuristic. See Trusted Types vs CSP.

---

## X-Content-Type-Options nosniff vs MIME Sniffing

- URL: https://roastmyopsec.com/blog/x-content-type-options-nosniff-vs-mime-sniffing
- Category: Safety
- Updated: 2026-08-05

X-Content-Type-Options nosniff vs MIME sniffing is a one-line header decision with a content-type homework assignment. nosniff tells supporting browsers not to guess a different MIME type than the server declared. That reduces MIME-confusion bugs where a file served as text/plain is treated as script. The header does not fix APIs that send the wrong Content-Type. RoastMyOpsec's free header audit looks for exact nosniff on the primary document. It does not inject polyglot files or exploit sniffing.

### The practical difference

MIME sniffing is a browser compatibility leftover: look at bytes, ignore the label. nosniff is the site saying 'believe the label.' User-uploaded content and 'download' endpoints are where sniffing used to hurt most.

A marketing homepage still wants nosniff. It is cheap, cacheable, and belongs next to HSTS and framing on the edge.

### How to choose what to ship

Send X-Content-Type-Options: nosniff on HTML and on anything users can upload or download. Set Content-Type correctly: HTML is text/html, JSON is application/json, JS is a JavaScript type — not text/plain for app bundles.

| Response | Content-Type | nosniff | Takeaway |
| --- | --- | --- | --- |
| Marketing HTML | text/html; charset=utf-8 | Required | Edge default for all documents |
| JSON API | application/json | Required | Stops 'this JSON is actually HTML' surprises |
| User uploads | True type; never text/html for untrusted files | Required | Header plus correct type plus CSP |
| Wrong type + no nosniff | Browser may guess | Missing | This is the MIME-confusion miss |

### When nosniff is not enough

Serving user HTML as text/html with nosniff is still XSS if that HTML runs. nosniff is not a content sanitizer. Pair uploads with storage that cannot execute and a CSP that does not need unsafe-inline.

APIs that declare text/html for error pages can still get framed or sniffed in old clients. Keep API errors as JSON.

### Common mistakes

The first mistake is nosniff on / and not on /assets/*.

The second mistake is Content-Type: text/plain for JavaScript because a tutorial did.

The third mistake is treating nosniff as a substitute for CSP.

### FAQ

**Is missing nosniff a critical vulnerability?**

Usually a hardening miss, not a standalone CVE. It matters more on hosts that serve user files or mixed content types. Still ship it everywhere.

**Does nosniff replace a correct Content-Type?**

No. It tells the browser to trust the type you sent. Send the right type.

**How does RoastMyOpsec check nosniff?**

It reads X-Content-Type-Options on the primary document as part of the free header pack. Header read only.

---

## www vs Apex Canonical Host for HTTPS Marketing Sites

- URL: https://roastmyopsec.com/blog/www-vs-apex-canonical-host-https
- Category: Safety
- Updated: 2026-07-27

www vs apex canonical host for HTTPS marketing sites is a single-source-of-truth decision, not a branding argument. Choose example.com or www.example.com, serve TLS on both if you must, and 301/308 the loser to the winner on HTTPS. Cookies, HSTS, canonical tags, sitemaps, and ads should name that winner. Two live hosts with different headers is how sessions split and HSTS includeSubDomains surprises you. RoastMyOpsec follows a bounded public redirect chain from the URL you submit. It does not hijack DNS or test Host-header tricks.

### The practical difference

The apex is the bare domain. www is a hostname that is usually a CNAME to the CDN. Either can be canonical. The miss is serving both as first-class sites with different CSP, cookies, or certificates.

Redirects must land on HTTPS. An HTTP apex that hops to HTTPS www is fine if the chain is short and the final host is the one in Search Console. A loop or a hop to a different registrable domain is a different finding — see open redirects.

### How to choose based on the stack

CDN in front: many teams canonical to www because the apex cannot CNAME at some DNS hosts (unless ALIAS/ANAME). Apex-first shops use ALIAS to the same edge. Pick one, document it, match the cert SANs (apex + www), then turn on HSTS on the canonical host. includeSubDomains only if every subdomain is ready — www is a subdomain of the apex; the apex is not a subdomain of www.

| Setup | What users type | What must be true | Takeaway |
| --- | --- | --- | --- |
| Canonical www | example.com → https://www… | Apex cert + redirect; cookies not Domain=.example.com unless you mean it | Common with CDN CNAME |
| Canonical apex | www → https://example.com | www cert + redirect; HSTS on apex covers www only with includeSubDomains | Fine if DNS ALIAS works |
| Both live, no redirect | Either | Two costumes | Fix this week |
| HTTP canonical, HTTPS extra | Either | Cleartext first hop | See HSTS vs redirect |

### What the roast can see

The submitted URL's redirect chain and whether the destination is HTTPS. Scan the host you actually advertise — and once more on the other hostname if both still answer. Cookie flags are graded on the landing Set-Cookie, which may be the canonical host only.

### Common mistakes

The first mistake is HSTS on www only while the apex still speaks HTTP.

The second mistake is Set-Cookie Domain=.example.com while you thought sessions were www-only.

The third mistake is sitemap and ads still listing the non-canonical host.

### FAQ

**Is www more secure than the apex?**

No. Security is TLS, HSTS, cookies, and one canonical host. www vs apex is operations, not a strength ranking.

**Should I scan both www and the apex?**

If both still return 200 without redirecting, yes — they are two public costumes. After a clean redirect, scanning the canonical URL is enough for headers.

**Does RoastMyOpsec pick my canonical host for me?**

No. It audits the URL you paste and follows bounded redirects. You choose the winner in DNS and the CDN.

---

## upgrade-insecure-requests vs HSTS for Mixed Content

- URL: https://roastmyopsec.com/blog/upgrade-insecure-requests-vs-hsts
- Category: Safety
- Updated: 2026-04-08

upgrade-insecure-requests vs HSTS for mixed content is a page-level rewrite versus a host-level memory. Content-Security-Policy: upgrade-insecure-requests tells supporting browsers to try HTTPS for http:// subresources on that document. Strict-Transport-Security tells them to skip HTTP for later navigations to the host (and includeSubDomains if you mean it). Neither deletes the http:// URLs in your HTML. Neither replaces a real redirect from :80 to :443. RoastMyOpsec parses the landing HTML for mixed-content http:// script, style, and image URLs and reads HSTS on the response. It does not score missing upgrade-insecure-requests as a finding and does not perform a man-in-the-middle test.

### The practical difference

UIR is CSP. It runs in the document that sent it. A third-party iframe with its own policy is not your UIR. HSTS is the host. After a trusted HTTPS response with a usable max-age, later visits to that host should not use cleartext — until max-age lapses or the user clears site data.

A first visit over HTTP still needs a redirect. UIR does not upgrade the document navigation itself the way HSTS preload can. See HSTS vs HTTPS redirect.

### How to choose what to ship

Fix the HTML: https:// for every first-party asset. Add HSTS when every included subdomain is ready. Add upgrade-insecure-requests as a belt while old CMS fields still emit http://. Do not preload HSTS to paper over mixed content. Do not set UIR and keep shipping active mixed scripts as a plan.

| Control | Scope | Fixes leftover http:// in HTML? | Takeaway |
| --- | --- | --- | --- |
| upgrade-insecure-requests | This document's subresources | Rewrites fetches; does not edit the CMS | Belt while you delete http:// |
| HSTS | Host (and subdomains if flagged) | No — navigations and memory | After HTTPS is real |
| HTTPS redirect | This request | No | Still required for the first hop |
| Delete http:// URLs | Your templates and CMS | Yes | The actual fix |

### What the roast can prove

Mixed-content hits are findings. Missing UIR is not. HSTS present or absent is graded as a header, not as proof that subresources upgraded. Pair with certificate expiry vs HSTS if max-age is heroic and renewal is not.

### Common mistakes

The first mistake is UIR without HSTS and calling the site 'HTTPS-only.'

The second mistake is HSTS includeSubDomains while a blog subdomain still serves HTTP assets.

The third mistake is treating UIR as a substitute for CSP script-src.

### FAQ

**Does upgrade-insecure-requests replace HSTS?**

No. UIR rewrites subresources on this page. HSTS remembers HTTPS for the host. You want the HTML fixed either way.

**Should brochure sites set UIR?**

Yes if old content still has http://. It is not a roast finding if you omit it after the HTML is clean.

**Does RoastMyOpsec require upgrade-insecure-requests?**

No. It looks for mixed-content URLs in landing HTML and reads HSTS. Missing UIR is not scored as a vulnerability.

---

## Trusted Types vs CSP for DOM XSS Defense

- URL: https://roastmyopsec.com/blog/trusted-types-vs-csp-for-dom-xss
- Category: Safety
- Updated: 2026-03-30

Trusted Types vs CSP for DOM XSS is two layers, not a replacement. Content-Security-Policy script-src (nonces, hashes, no unsafe-inline) limits which scripts load. Trusted Types (CSP trusted-types and require-trusted-types-for 'script') limits assigning untrusted strings into sinks like innerHTML in supporting browsers. A brochure site with three first-party files should enforce CSP before considering Trusted Types. A SPA that still concatenates HTML in the client is the audience for Trusted Types — after the policy is real, not Report-Only cosplay. RoastMyOpsec reads CSP quality signals on the primary HTML response. It does not inject DOM XSS and it does not grade a full Trusted Types policy as a pass/fail today.

### The practical difference

CSP is mostly about what may execute as a script or load as a frame. Trusted Types is about how JavaScript is allowed to write into the document. You can have a tight script-src and still innerHTML a reflected string. You can have Trusted Types and still load a hostile script if CSP is wide open.

Browser support is uneven. Treat Trusted Types as progressive hardening, not a checkbox for every marketing CMS.

### How to choose based on the stack

WordPress brochure: enforce CSP (including frame-ancestors), skip Trusted Types until a custom theme is actually doing DOM writes. React/Vue with a compiler that avoids innerHTML: CSP first; Trusted Types if you have remaining sinks. Legacy jQuery HTML strings: plan Trusted Types in the app, not as a CDN header copied from a lab.

| Control | Stops | Does not stop | Takeaway |
| --- | --- | --- | --- |
| Enforcing CSP script-src | Unexpected script URLs / inline without nonce | DOM writes by an allowed script | Do this first |
| Trusted Types | Raw strings into supported sinks | Logic bugs; unsupported browsers | App hardening after CSP |
| Report-Only CSP | Nothing | Everything | See report-only vs enforcing |
| HttpOnly cookies | JS reading the session cookie | DOM XSS acting as the user | Still required |

### What the roast can see

CSP header presence and looseness (unsafe-inline, wild hosts). A trusted-types directive in CSP may show up as a quality signal later; do not wait for the scanner to invent a Trusted Types program. Pair with nonces vs hashes.

### Common mistakes

The first mistake is Trusted Types with default-src * and unsafe-inline.

The second mistake is a policy that allows a duplicate 'default' policy that accepts every string.

The third mistake is enabling require-trusted-types-for on a CMS you do not compile.

### FAQ

**Does Trusted Types replace CSP?**

No. Enforce CSP first. Add Trusted Types when your JavaScript still writes HTML strings into the DOM.

**Should every marketing site ship Trusted Types?**

Usually no. Finish framing, HSTS, and a real script-src. Trusted Types is for apps with DOM sinks, not for a static brochure.

**Does RoastMyOpsec test DOM XSS?**

No. Header read only. No payloads, no sink fuzzing.

---

## TLS-RPT vs MTA-STS for SMTP Failure Reports

- URL: https://roastmyopsec.com/blog/tls-rpt-vs-mta-sts-for-email
- Category: Safety
- Updated: 2026-03-13

TLS-RPT vs MTA-STS for SMTP failure reports is telemetry versus policy. MTA-STS (RFC 8461) publishes how sending MTAs should use TLS to your MX. TLS-RPT (RFC 8460) is a DNS TXT on _smtp._tls that names a mailbox or HTTPS endpoint for JSON reports when TLS to that MX fails or policy cannot be applied. DMARC rua is a different report stream about From alignment. A mailbox you never read is not a control. RoastMyOpsec does not query _smtp._tls, does not fetch MTA-STS policy, and does not score missing TLS-RPT as an F.

### The practical difference

MTA-STS is the lock you asked senders to honor. TLS-RPT is the smoke alarm. DMARC reports are about spoofing, not SMTP TLS. Website NEL and CSP reports are browser telemetry — different layer. See MTA-STS vs DMARC and NEL vs CSP reports.

HTTPS rua endpoints for TLS-RPT must not require cookies and must not be an open redirect. Keep the mailbox on a domain you actually operate.

### How to choose a policy

Ship SPF/DKIM/DMARC first. Add MTA-STS when you control the MX. Add TLS-RPT the same week so you see senders that cannot TLS. Start MTA-STS in testing with reports, then enforce. Do not publish rua to a shared inbox that auto-deletes JSON.

| Control | Job | Failure mode | Takeaway |
| --- | --- | --- | --- |
| MTA-STS | SMTP TLS policy to MX | Stale HTTPS policy host | The actual transport policy |
| TLS-RPT | Reports when TLS/policy fails | Mailbox you ignore | Telemetry; not a roast F |
| DMARC rua | From-alignment reports | Unmonitored rua | Different stream |
| Website HTTPS | Browser to origin | Unrelated to MX | Different layer |

### When TLS-RPT wins

TLS-RPT wins once MTA-STS exists and someone will read the JSON. MTA-STS still wins as the policy. Missing TLS-RPT on a domain that does not send or receive mail is healthy. Pair with DNS hygiene if _smtp._tls is a dangling CNAME.

### What the roast can prove

Nothing about MX or TLS-RPT. The scan is a consented GET to the URL you paste. Confirm TXT records in your mail console. Pair with BIMI only after DMARC is real — that is branding, not SMTP TLS.

### Common mistakes

The first mistake is TLS-RPT without MTA-STS and calling the reports a policy.

The second mistake is a rua URL that 302s through a marketing tracker.

The third mistake is treating missing TLS-RPT as a website OPSEC F.

### FAQ

**Does TLS-RPT replace MTA-STS?**

No. Reports describe failures. MTA-STS is the policy senders should follow.

**Should every marketing domain publish TLS-RPT?**

Only if you operate MX and will read the reports. Missing it is not a roast F.

**Does RoastMyOpsec check TLS-RPT?**

No. It does not query _smtp._tls or parse SMTP TLS reports.

---

## Storage Access API vs Partitioned Cookies for Embeds

- URL: https://roastmyopsec.com/blog/storage-access-api-vs-partitioned-cookies
- Category: Safety
- Updated: 2026-01-04

Storage Access API vs partitioned cookies for embeds is two answers to third-party cookie restrictions. Partitioned cookies (CHIPS) keep a SameSite=None cookie in a jar keyed by the top-level site, so site A and site B do not share the embed's identity. The Storage Access API lets an embedded origin request access to its unpartitioned first-party cookies after a user gesture, typically for a documented widget that already has a first-party relationship. Neither is a reason to mark the main login cookie None. Brochure sites that are not embeds should ignore both. RoastMyOpsec grades Set-Cookie on a consented GET. It does not call requestStorageAccess, does not load you in a third-party iframe, and does not prove CHIPS isolation.

### The practical difference

Partitioned is a cookie attribute: if you send it third-party, isolate it per top-level site. Storage Access is a JavaScript prompt-shaped API: the embed asks to use its unpartitioned cookies in this embedding context after the user interacts. Unpartitioned None without either is the old shared tracker jar.

If the cookie is wordpress_logged_in on your own origin, keep Lax or Strict. See SameSite=None vs Lax and partitioned cookies vs None.

### How to choose based on the embed's job

You are not an embed: do not set Partitioned on session cookies and do not call the Storage Access API. Widget that must remember a preference per host: Partitioned + None + Secure, dedicated name. Widget that must continue a first-party login inside a partner iframe after a click: Storage Access on that flow, still CSRF-defend state changes, still not the marketing homepage session. Marketing pixels: prefer first-party analytics instead of either knob.

| Control | Jar | When it wins | Takeaway |
| --- | --- | --- | --- |
| Lax / Strict session | First-party | Your site's login | Default |
| None; Secure; Partitioned | Per top-level site | Widget cookie that must exist in iframes | Better than unpartitioned None |
| Storage Access API | Unpartitioned, after gesture | Documented embed with a first-party relationship | Not a tracker toggle |
| Unpartitioned None, no API | Shared across sites | Legacy you have not migrated | High CSRF and tracking cost |

### When Storage Access wins

Storage Access wins when the user already has a first-party account with the embed origin and a click is a reasonable gate. Partitioned wins when you only need a per-host widget cookie and should not join a global profile. First-party Lax still wins for the brand's own session.

### What the roast can prove

Landing Set-Cookie flags, including Partitioned if present. Storage Access is a runtime API — it will not show on a header scan. Confirm embed cookies in your own staging iframe. Pair with third-party trackers vs first-party analytics if the 'embed' is a pixel.

### Common mistakes

The first mistake is calling requestStorageAccess to restore a tracking cookie the browser already partitioned.

The second mistake is Partitioned plus Storage Access plus None on the same session cookie 'to be safe.'

The third mistake is treating a roast miss on Partitioned as proof the embed jar is correct.

### FAQ

**Do brochure sites need the Storage Access API?**

No. It is for embedded origins that must reach unpartitioned cookies after a user gesture.

**Is Storage Access the same as CHIPS?**

No. CHIPS partitions the cookie jar. Storage Access requests the unpartitioned jar in an embed after interaction.

**Does RoastMyOpsec call requestStorageAccess?**

No. It reads Set-Cookie on a consented GET. Confirm embed behavior in your own staging iframe.

---

## SPF vs DKIM vs DMARC for Domain Email OPSEC

- URL: https://roastmyopsec.com/blog/spf-vs-dkim-vs-dmarc-email-opsec
- Category: Safety
- Updated: 2025-12-18

SPF vs DKIM vs DMARC is a stack decision, not a pick-one DNS beauty contest. SPF lists which servers may send mail for the domain. DKIM signs messages so receivers can verify they were not altered in transit. DMARC tells receivers what to do when SPF/DKIM fail alignment, and it is the policy layer that actually changes spoofing outcomes. A public site with a custom domain used in From: lines needs all three. RoastMyOpsec looks up public SPF and DMARC TXT records on the apex — it does not send mail or test inboxes.

### The practical difference

SPF answers 'may this IP send as this domain?' It breaks when you add a new ESP and forget to update the record, or when you hit the DNS lookup limit.

DKIM answers 'did this exact message come from a key we publish?' It survives forwarding better than SPF in many paths, which is why DMARC alignment cares about both.

DMARC answers 'if this mail fails, should the receiver monitor, quarantine, or reject — and where do reports go?' Without DMARC, SPF and DKIM are homework with no teacher.

### How to choose a rollout order

Publish SPF for current senders. Turn on DKIM at each ESP. Add DMARC at p=none with a rua mailbox you will actually read. Move to quarantine, then reject, once legitimate mail authenticates.

Do not jump to p=reject on day one if marketing, billing, and a founder Gmail all send as the domain. Inventory senders first.

| Record | Job | Failure mode | Takeaway |
| --- | --- | --- | --- |
| SPF | Authorize sending IPs / includes | Too many lookups; missing ESP include | Keep the include list current |
| DKIM | Cryptographic signature | Selector not published; mail rewritten | Enable per provider, not once globally |
| DMARC | Policy + reporting + alignment | p=none forever; no monitoring | Policy is what changes attacker success |

### When a marketing site still needs this

Even if the website is static, the domain is a costume. Invoice fraud and fake 'CEO' mail work because customers trust the domain they typed into a browser.

If you do not send mail, you can still publish a tight SPF (-all) and a DMARC reject policy so nobody else sends either. Confirm with whoever owns Google Workspace, Microsoft 365, or the ESP before you lock it.

### Common mistakes

The first mistake is SPF ~all with a false sense of safety and no DMARC.

The second mistake is a DMARC record with nowhere to send reports.

The third mistake is authenticating the root domain and forgetting a sending subdomain used by the ESP.

### FAQ

**Do I need SPF, DKIM, and DMARC or just one?**

You need the stack. SPF and DKIM provide signals. DMARC is the policy that tells receivers what to do and whether those signals must align with the From domain.

**What DMARC policy should a small site start with?**

Start at p=none, read aggregate reports, fix failing sources, then quarantine, then reject. Skip ahead only if you are sure every sender is authenticated.

**Can a URL scan prove DKIM works?**

A public DNS lookup can show SPF and DMARC records. DKIM selectors are per-provider. RoastMyOpsec does not send test messages; confirm DKIM in your ESP and in DMARC reports.

---

## Speculation Rules vs dns-prefetch for Marketing Sites

- URL: https://roastmyopsec.com/blog/speculation-rules-vs-dns-prefetch
- Category: Safety
- Updated: 2025-12-10

Speculation Rules vs dns-prefetch for marketing sites is how aggressively the browser prepares the next hop. dns-prefetch (and preconnect) resolve or warm a host. Speculation Rules (JSON in a script type or a Speculation-Rules header) can prefetch or prerender listed URLs. Prerender is not a DNS hint: supporting browsers may load the document with cookies and run work as if the user already clicked. Do not list /account, /checkout, or /reset?token= . Referrer-Policy still applies to hinted navigations. RoastMyOpsec does not score Speculation Rules, dns-prefetch, or HTTP 103. It does not prerender your site. Missing speculation is not an OPSEC F.

### The practical difference

dns-prefetch leaks that a hostname exists to the resolver slightly earlier. prerender can leak the full page fetch — and any first-party cookies the browser would send — to that URL before the click. prefetch is in between: it may fetch without fully running the page, still without listing secrets in the URL.

HTTP 103 Early Hints is a CDN-speed cousin for Link preload. Speculation Rules are document or header policy for navigations. See Early Hints vs CSP.

### How to choose what to speculate

Public article-to-article and homepage-to-pricing: prefetch or prerender if the URLs are cacheable and cookie-boring. Logged-in app shells: skip prerender. Tokenized links: never. eagerness conservative unless you measured the privacy trade.

| Hint | What it does | Cookie / token risk | Takeaway |
| --- | --- | --- | --- |
| dns-prefetch / preconnect | Warm DNS or TLS to a host | Hostname leak, not the path | See dns-prefetch vs Referrer-Policy |
| prefetch (speculation) | Fetch a document early | URL and cookies on that GET | Public pages only |
| prerender | Nearly run the next page | Highest — treat like a visit | No auth, no tokens in the URL |
| 103 Link preload | Subresources before HTML | URLs in the Link header | See Early Hints vs CSP |

### What the roast can prove

Absence of Speculation-Rules is not a finding. A public HTML page that speculates /reset?token= is a product bug you should catch in review — the roast does not parse the JSON. Pair with tokens in URLs and Referrer-Policy.

### Common mistakes

The first mistake is prerendering every internal path including /admin.

The second mistake is listing signed CloudFront URLs in speculation rules.

The third mistake is treating speculation as a security header checklist item.

### FAQ

**Does prerender bypass SameSite cookies?**

No. It is still a navigation-class fetch from the user's browser. The risk is fetching a sensitive URL earlier than the click, not skipping cookie rules.

**Should brochure sites enable Speculation Rules?**

Only for public next-clicks you would be fine fetching immediately. Missing them is not a roast F.

**Does RoastMyOpsec score Speculation Rules?**

No. It does not prerender pages or parse speculation JSON.

---

## Set-Cookie Priority vs Max-Age for Cookie Retention

- URL: https://roastmyopsec.com/blog/set-cookie-priority-vs-max-age
- Category: Safety
- Updated: 2025-11-06

Set-Cookie Priority vs Max-Age is two different clocks on the same cookie, and only one is a web standard: Max-Age (or Expires) is how long the cookie should live, while Priority=High|Medium|Low is a Chromium eviction hint when a per-domain jar is full — not a security flag and not a substitute for Max-Age. Auth cookies still need Secure, HttpOnly, and SameSite. RoastMyOpsec does not grade Priority, does not fail Max-Age versus a session cookie, and only checks those three flags on session-like landing names.

### The practical difference

Max-Age is seconds until expiry. Session cookies omit Max-Age and Expires. Browsers also cap very long lifetimes. Deleting a cookie is Max-Age=0 (or a past Expires) with the same Path and Domain as the original.

Priority does not make a cookie HttpOnly, does not keep it off HTTP, and does not change SameSite. A High-priority analytics cookie is still not your session. Non-Chromium browsers ignore Priority. Do not advertise Priority=High as hardening in a security review.

### How to choose based on what you shipped

Set Max-Age from the product lifetime. Set Secure, HttpOnly, and SameSite from the cookie's job. Leave Priority unset unless you are tuning Chromium eviction for a large first-party jar and you understand it will not travel to every browser.

If the jar is overflowing, fewer cookies is the real fix — not Priority=High on everything. Session-shaped names should stay few, prefixed when you can, and never duplicated as a JS-readable twin.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Max-Age (or Expires) | You want a defined lifetime or an immediate delete | Huge lifetimes; Path mismatch on logout | The standard retention control |
| Session cookie (no Max-Age) | You want browser-session semantics | Session restore; not a lock | Lifetime choice, still needs flags |
| Priority=High (Chromium) | Eviction tuning when the domain jar is full | Treating it as Secure or as a standard | Ignore for OPSEC; not scored |
| Secure + HttpOnly + SameSite | Any cookie that proves login | Flags on a pixel, missing on session | What a public roast can actually grade |

### When Max-Age still wins and Priority does not

Max-Age always wins as the lifetime contract you can document. Priority never wins as a substitute for flags or as a reason to keep fifty marketing cookies. If Chromium evicts a Low-priority preference cookie, that is a jar-hygiene problem, not a session design.

A homepage roast often never sees the login Set-Cookie. Audit the auth response in your own tools for flags and lifetime. Prefixes (__Host- / __Secure-) are a better name-level lock than Priority.

### Common mistakes

The first mistake is Priority=High on the session cookie and missing HttpOnly.

The second mistake is documenting Priority as a W3C security header.

The third mistake is reading a cookie pass on the marketing pixel as a pass on login Max-Age.

### What a URL roast can prove

RoastMyOpsec grades session-like Set-Cookie headers on the URL you paste for Secure, HttpOnly, and SameSite (and SameSite=None without Secure). It does not score cookie Max-Age versus session as a standalone F, does not read Priority, and does not log in. No cookies on the landing response is info, not a lifetime certificate. Headers still cover CSP quality, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy; TLS is HTTPS scheme; plus paths and public JS secrets. No GPC score, no HSTS preload submit, no exploit payloads.

### FAQ

**Does Priority=High make a cookie more secure?**

No. It is a Chromium eviction hint. Security attributes are Secure, HttpOnly, SameSite, Path, Domain, and prefixes — not Priority.

**Does RoastMyOpsec check cookie Priority or Max-Age?**

No. It does not grade Priority. Max-Age versus a session cookie is not a standalone fail. It checks Secure, HttpOnly, and SameSite on session-like names.

**Should I set Priority on auth cookies?**

You do not need it for OPSEC. Set lifetime with Max-Age if you want a clock, and set the standard flags. Priority will not travel to every browser.

---

## SameSite=Strict vs Lax for Login Cookies

- URL: https://roastmyopsec.com/blog/samesite-strict-vs-lax-for-login
- Category: Safety
- Updated: 2025-08-29

SameSite=Strict vs Lax for login cookies is when the browser attaches that cookie on a request that started on another site: Lax still sends it on top-level GET navigations, while Strict withholds it on all cross-site requests so an inbound email or ads click often looks logged out. Most public logins should ship Lax with Secure and HttpOnly; reserve Strict for high-sensitivity apps that can live without the cookie on that first hop. RoastMyOpsec checks that SameSite is present on session-like landing cookies — it does not fail Lax, does not prefer Strict, and does not POST CSRF proofs.

### The practical difference

Lax is the modern default story for first-party sessions: cross-site subresource and classic cross-site POST usually omit the cookie; a top-level GET to your site still sends it so 'open this link' still sees the session. Strict is a tighter belt: the cookie stays on same-site requests only, which is why return-from-IdP, magic links, and some checkout success URLs look anonymous until a second same-site click.

SameSite=None is a different product (cross-site embeds) and requires Secure. Do not mark the login cookie None because a widget asked. Lax is not a complete CSRF program for every state-changing POST; keep tokens or Origin checks on those forms.

### How to choose based on what you shipped

Brochure plus WordPress login, SaaS marketing site with an app on the same registrable domain, and most OAuth returns: SameSite=Lax on the session name. Banking-style or internal admin where users always type the URL: Strict can win after you test email links and payment redirects.

If Strict breaks a real top-level return, do not 'fix' it with None on the same name. Use Lax, or a short bounce on your own origin that then navigates same-site.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| SameSite=Lax | Public login, email links, ads-to-app hops, most OAuth returns | Not a full CSRF substitute for every POST | Default for session cookies on marketing-plus-app origins |
| SameSite=Strict | High-sensitivity session, users land same-site, no inbound cookie needed | First request from another site looks logged out | Ship only after testing payment and magic-link returns |
| SameSite=None; Secure | A dedicated embed or SSO cookie that must travel cross-site | The main session name wearing None | Different cookie, not a Strict/Lax compromise |
| Missing SameSite | Never for new auth cookies | Browser defaults differ and change | Set Lax or Strict on purpose |

### When SameSite=Strict still wins

Strict still wins when the login cookie must never ride an inbound cross-site GET — admin consoles, high-value sessions, users who bookmark the app. Pair it with a same-site interstitial if you still send mail links: the mail hits a cookieless landing, then a same-site continue sets the session in view.

Lax still wins for almost every public brand login. Treating Strict as a roast requirement is a misread: the free cookie check wants SameSite present on session-like names, not a particular value.

### Common mistakes

The first mistake is SameSite=Strict on the session and then SameSite=None 'so Google ads work' on the same name.

The second mistake is Lax as the only CSRF control on JSON APIs that accept cross-site POSTs from old browsers.

The third mistake is assuming a homepage roast saw the login cookie when only a marketing pixel was set.

### What a URL roast can prove

A RoastMyOpsec URL roast reads the primary response: CSP quality signals, HSTS, framing, Referrer-Policy, nosniff, Permissions-Policy, HTTPS scheme, session-like cookie flags, sensitive paths, and public JS secrets. For cookies it checks Secure, HttpOnly, and SameSite on session-shaped names — including SameSite=None without Secure. It does not score Strict versus Lax as a standalone F, does not score cookie Max-Age versus session, and does not log in. Missing SameSite is the cookie finding; picking Lax is not. No exploit payloads.

### FAQ

**Should my login cookie be SameSite=Strict or Lax?**

Lax is the usual public-login default. Use Strict only when you can live without the cookie on inbound cross-site top-level GETs and you have tested mail and payment returns.

**Does RoastMyOpsec require SameSite=Strict?**

No. It checks that SameSite is set on session-like landing cookies. Lax satisfies that check. It does not send CSRF proofs.

**Is Lax enough without CSRF tokens?**

Lax cuts a class of cross-site POSTs. Keep CSRF tokens or strict Origin checks on state-changing forms, especially if you still support older clients.

---

## SameSite=None vs Lax for Third-Party Embeds

- URL: https://roastmyopsec.com/blog/samesite-none-vs-lax-for-embeds
- Category: Safety
- Updated: 2025-08-21

SameSite=None vs Lax for third-party embeds is a cookie-attachment decision, not a branding toggle. SameSite=Lax (or Strict) keeps the session cookie off most cross-site POSTs and off many cross-site navigations. SameSite=None requires Secure and is how a cookie is sent in a cross-site iframe or other third-party context. Marketing homepages should keep auth cookies on Lax or Strict. If an embed must authenticate, use a dedicated embed cookie with None+Secure — not the main session name. Pair with CSRF tokens for state-changing forms. RoastMyOpsec grades session-like Set-Cookie flags on the URL you paste. It does not log in, does not load third-party iframes as a user, and does not send CSRF PoCs.

### The practical difference

Lax is the modern default story for first-party sessions: top-level GETs may still send the cookie; classic cross-site POSTs often will not. Strict is tighter and can break return-from-payment flows. None is 'always send on HTTPS cross-site' — which is what trackers wanted, and what CSRF loves if that cookie is also your vault key.

Chrome's third-party cookie changes make None less of a tracker free-for-all, but a session cookie marked None is still the wrong shape for a brochure login.

### How to choose based on the cookie's job

Session / auth: Lax or Strict, HttpOnly, Secure, preferably __Host-. Preference or theme cookies: Lax is fine; they should not authorize anything. Embed that must know the user inside another site's iframe: a separate cookie, None+Secure, partitioned if you use CHIPS, never the same name as wordpress_logged_in.

| SameSite | Typical use | Cross-site iframe | Takeaway |
| --- | --- | --- | --- |
| Lax | First-party session | Usually not sent | Default for login cookies |
| Strict | High-sensitivity session | Not sent | Test checkout return URLs |
| None; Secure | Explicit embed/SSO cookie | Sent on HTTPS | Dedicated name; still CSRF-defend |
| None on the session cookie | Widget asked nicely | Session rides along | Don't |

### What the roast can see

Landing Set-Cookie attributes only. Cookies issued after login are often invisible. A marketing pixel cookie with None is a privacy/OPSEC smell; an auth cookie with None is a finding. See HttpOnly vs Secure vs SameSite and CSRF vs SameSite.

### Common mistakes

The first mistake is SameSite=None on every cookie 'for Safari.'

The second mistake is None without Secure, which browsers reject.

The third mistake is treating Lax as a complete CSRF defense for every POST.

### FAQ

**Should my session cookie be SameSite=None?**

Almost never. Use Lax or Strict for login. Reserve None+Secure for a cookie whose only job is a documented cross-site embed.

**Is SameSite=Lax enough without CSRF tokens?**

Lax reduces a class of cross-site POSTs. You still want CSRF defenses on state-changing forms. See the CSRF vs SameSite guide.

**Will RoastMyOpsec see my embed cookie?**

Only if Set-Cookie is on the audited URL's response. Login-issued cookies are often missing from a public landing roast.

---

## Referrer-Policy vs URL Leaks on Public Websites

- URL: https://roastmyopsec.com/blog/referrer-policy-vs-url-leaks
- Category: Safety
- Updated: 2025-06-22

Referrer-Policy vs URL leaks is a default you should set on purpose. When a user clicks from your HTTPS page to another origin, the browser may send a Referer header that includes path and query. If those URLs hold tokens, reset codes, or internal search strings, you just mailed them to a third party. strict-origin-when-cross-origin is a sane default for most public sites: full referrer on same-origin, origin-only on HTTPS cross-origin, nothing when downgrading to HTTP. RoastMyOpsec's free header audit grades Referrer-Policy on the primary document. It does not follow your users off-site.

### The practical difference

The Referer header is a browser feature that tells the next site where the click came from. Referrer-Policy is how you constrain that story. no-referrer sends nothing. unsafe-url sends everything, including to HTTP.

Meta referrer tags exist, but HTTP headers at the edge cover every HTML route more reliably.

### How to choose a policy

Marketing sites that need analytics on outbound clicks can often live with origin-when-cross-origin or strict-origin-when-cross-origin. Apps that put secrets in query strings should stop putting secrets in query strings first, then set a tight policy as defense in depth.

| Policy | Same-origin | HTTPS → HTTPS cross-origin | Takeaway |
| --- | --- | --- | --- |
| no-referrer | Nothing | Nothing | Maximum privacy; some analytics break |
| strict-origin-when-cross-origin | Full URL | Origin only | Usual default for public sites |
| origin | Origin only | Origin only | When paths should never leave |
| unsafe-url | Full URL | Full URL, even to HTTP in some cases | Do not ship this on tokenized links |

### When policy is not the real bug

Password-reset and magic-link URLs in query strings leak through logs, browser history, and Referer regardless of a 'pretty good' policy. Move those tokens to POST bodies or fragment identifiers you never send to the server of a third party.

Open redirects plus a fat Referer is how a token walks off-site. Collapse redirect chains on a host you own.

### Common mistakes

The first mistake is no Referrer-Policy at all, trusting browser defaults forever.

The second mistake is unsafe-url because an old analytics vendor asked for full paths.

The third mistake is a tight policy on / and none on /blog/*.

### FAQ

**What Referrer-Policy should a marketing site use?**

strict-origin-when-cross-origin is the usual starting point. Tighten to origin or no-referrer if paths are sensitive.

**Does Referrer-Policy stop tokens in my own URLs?**

It reduces who else sees them on outbound clicks. It does not stop your own logs, analytics, or the user's history. Do not put secrets in query strings.

**How does RoastMyOpsec grade Referrer-Policy?**

As part of the free header pack on the primary document response. Header read only.

---

## Referrer-Policy no-referrer vs strict-origin

- URL: https://roastmyopsec.com/blog/referrer-policy-no-referrer-vs-strict-origin
- Category: Safety
- Updated: 2025-06-14

Referrer-Policy no-referrer vs strict-origin is how much of the current URL the browser may put in Referer: no-referrer sends nothing, while strict-origin sends only the origin on HTTPS-to-HTTPS hops and nothing on a downgrade to HTTP. Most public sites want a cross-origin trim without killing same-origin full URLs — that is often strict-origin-when-cross-origin, not these two extremes. RoastMyOpsec grades that a Referrer-Policy header is present on the primary document; it does not score the token you chose, follow users off-site, or score GPC.

### The practical difference

no-referrer is a mute button. Cross-origin analytics, CDN hotlink logs, and some payment or affiliate pixels that key off Referer get an empty story. It is the right control when paths or query strings must never leave, including to your own third-party tags.

strict-origin never sends path or query. Same-origin fetches also get origin-only, which is stricter than strict-origin-when-cross-origin (full URL same-origin, origin-only cross-origin HTTPS, nothing on downgrade). unsafe-url is the opposite miss. Header at the edge beats a meta tag that only some HTML routes remember.

### How to choose based on what you shipped

Apps that still put reset tokens or invite codes in query strings should stop doing that, then set no-referrer or origin-only policies as defense in depth. Marketing sites that need outbound-click analytics usually start at strict-origin-when-cross-origin; step to strict-origin if same-origin paths should stay home too.

If a vendor demands full Referer, treat that as a data-sharing decision, not a default. Do not leave the header off and hope browser defaults stay kind.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| no-referrer | Tokens in URLs, high privacy, no Referer-dependent pixels | Some analytics and CDNs see an empty Referer | Maximum mute; fix query-string secrets anyway |
| strict-origin | You never want path or query in Referer, even same-origin | Same-origin full-URL analytics and some debug traces break | Origin-only everywhere HTTPS-to-HTTPS |
| strict-origin-when-cross-origin | Usual public-site default | Same-origin still sends path/query to your own hosts | Start here unless paths are sensitive on-origin too |
| unsafe-url | Almost never | Full URL, including to weaker contexts depending on policy | Do not ship on tokenized or account URLs |

### When no-referrer still wins

no-referrer still wins on password-reset landings, magic-link bounce pages, and any HTML that might still carry a secret in the URL bar. strict-origin still wins when you need a vendor to see that traffic came from your origin without donating paths.

Neither policy stops your own server logs, the user's history, or a copy-pasted URL. Move secrets out of query strings first.

### Common mistakes

The first mistake is no Referrer-Policy header and a comment that 'Chrome defaults are fine.'

The second mistake is no-referrer on the blog and unsafe-url on checkout because a pixel asked.

The third mistake is a tight policy on / and none on /blog/* behind a different CDN cache key.

### What a URL roast can prove

RoastMyOpsec's free header audit flags a missing Referrer-Policy on the primary HTML response. It does not grade no-referrer versus strict-origin versus strict-origin-when-cross-origin, does not score Global Privacy Control (GPC), and does not crawl outbound links to see what Referer a partner received. Pair the header with honest URLs: tokens in query strings still leak through logs and history. The same pass still checks CSP quality, HSTS, framing, nosniff, Permissions-Policy, HTTPS scheme, session-like cookies, paths, and public JS secrets — header read only, no exploit payloads.

### FAQ

**Is no-referrer better than strict-origin?**

no-referrer donates less. strict-origin keeps origin-level analytics on HTTPS hops. Pick from the page's secrets and your measurement needs, not from a scanner's vanity score.

**Does RoastMyOpsec require a specific Referrer-Policy value?**

No. It checks that the header is present. Choosing no-referrer or strict-origin is your product decision.

**Does Referrer-Policy replace removing secrets from URLs?**

No. It only constrains the Referer header. History, logs, and screenshots still see the address bar. Do not put session material in query strings.

---

## Private Network Access vs CORS for Browser APIs

- URL: https://roastmyopsec.com/blog/private-network-access-vs-cors
- Category: Safety
- Updated: 2025-04-07

Private Network Access vs CORS for browser APIs is two different gates. CORS (Access-Control-Allow-Origin) decides whether a web page may read a cross-origin HTTP response. Private Network Access (PNA, evolving as Local Network Access in Chromium) is an extra browser rule: a page on the public internet should not casually fetch RFC1918, localhost, or other less-public addresses without a dedicated preflight and a target that opts in. Wildcard CORS on an internal API is still a miss; PNA is not a substitute for locking that API off the public origin. RoastMyOpsec refuses to scan localhost and private IPs. It does not send PNA preflights, does not map your LAN, and does not score missing PNA headers as an F.

### The practical difference

CORS is origin-to-origin on the web. PNA is public-to-private (and similar) address space. A marketing page that fetch()es http://192.168.1.1/ to 'detect the router' is the anti-pattern these rules exist to shrink. WebSockets and JSONP are other costumes — see those guides. Consent-based scanning of URLs you own is a different product than a page probing the visitor's LAN.

Internal admin APIs belong on VPN, mTLS, or a hostname that is not reachable from random laptops on café Wi-Fi. Do not rely on 'nobody knows the IP.'

### How to choose a policy

Public marketing site: do not fetch private IPs or localhost from page JavaScript. Device setup flows that must talk to a local appliance: follow current browser PNA/Local Network Access guidance, user-visible permission, and a target that only opts in for that product. CORS allowlists stay named origins. RoastMyOpsec customers paste public HTTPS URLs — not 10.0.0.0/8.

| Control | What it gates | Public site default | Takeaway |
| --- | --- | --- | --- |
| CORS ACAO | Cross-origin read of HTTP | Named origins, not * | See CORS wildcard vs allowlist |
| Private Network Access | Public page → private address | Do not fetch LAN/localhost | Not a roast header check |
| Firewall / VPN | Who can reach the API | Internal APIs stay internal | The real lock |
| URL roast scope | Consented public HTTPS | No private IPs | See consent-based scan |

### When PNA still wins

PNA wins as a browser brake when a XSS gadget on a public origin tries to talk to the visitor's LAN. CORS still wins for ordinary API reads. Neither replaces keeping admin off the public internet. Pair with consent-based scan vs unsolicited probing.

### What the roast can prove

ACAO on a bounded untrusted Origin can surface for public URLs. PNA headers are not scored. Private targets are rejected before a fetch. Pair with CORS credentials vs wildcard if the internal API also reflects Origin.

### Common mistakes

The first mistake is a public SPA that probes localhost 'to see if the desktop agent is running' without a current PNA design.

The second mistake is Access-Control-Allow-Origin * on an RFC1918 API.

The third mistake is treating a roast refusal of a private IP as a scanner bug.

### FAQ

**Does CORS replace Private Network Access?**

No. CORS is cross-origin reads. PNA is an extra gate for public pages reaching private addresses.

**Will RoastMyOpsec scan my printer or localhost?**

No. Localhost and private IPs are blocked. Paste a public origin you own.

**Does RoastMyOpsec send PNA preflights?**

No. It does not map LANs or score PNA headers.

---

## Permissions-Policy vs Device APIs for Marketing Sites

- URL: https://roastmyopsec.com/blog/permissions-policy-vs-device-apis
- Category: Safety
- Updated: 2025-02-06

Permissions-Policy vs device APIs is a default-deny choice for public HTML. The header (formerly Feature-Policy) tells the browser which powerful APIs this document and its iframes may use. A brochure site should disable camera, microphone, geolocation, and payment unless a specific page needs them. XSS or a third-party script cannot casually turn on the webcam if the policy forbids it. RoastMyOpsec's free header audit grades Permissions-Policy on the primary document. It does not prompt for device access or test exploits.

### The practical difference

A permission prompt is a user decision. Permissions-Policy is a site decision that can make the prompt impossible. Feature-Policy was the old name; send Permissions-Policy on modern stacks.

CSP limits what scripts run. Permissions-Policy limits what those scripts may ask the device to do. You want both.

### How to choose what to disable

Start from the page's job. Home, blog, and docs: deny camera, microphone, geolocation, payment, USB, and similar. A store checkout may allow payment on that route only. A maps store-locator may allow geolocation on that route only — not site-wide.

| Page type | camera / mic | geolocation | payment | Takeaway |
| --- | --- | --- | --- | --- |
| Marketing / blog | Off | Off | Off | One edge header covers the costume |
| Store locator | Off | On that path if needed | Off | Do not enable maps APIs on /about |
| Checkout | Off | Off unless required | On that path if you use Payment Request | Scope the header by route |
| Support video widget | Only if the vendor needs it | Off | Off | Prefer the vendor iframe with its own policy |

### When an empty policy still fails

No Permissions-Policy header means browser defaults, which are looser than a marketing site needs. Third-party iframes can inherit more than you think unless you set allow= on the iframe and a document policy.

Tag managers that inject widgets are why this belongs next to the tracker guide: fewer scripts, tighter policy.

### Common mistakes

The first mistake is enabling geolocation site-wide for one store-finder page.

The second mistake is copying a checkout Permissions-Policy onto the blog.

The third mistake is Feature-Policy only, with no Permissions-Policy, on current Chrome.

### FAQ

**What Permissions-Policy should a public website send first?**

Disable camera, microphone, geolocation, and payment unless a named route needs them. Expand only with a product reason.

**Does Permissions-Policy replace CSP?**

No. CSP constrains script and framing sources. Permissions-Policy constrains device and browser features. Ship both.

**How does RoastMyOpsec grade Permissions-Policy?**

As part of the free header pack on the primary HTML response. Header read only — no device prompts.

---

## Permissions-Policy unload vs beforeunload

- URL: https://roastmyopsec.com/blog/permissions-policy-unload-vs-beforeunload
- Category: Safety
- Updated: 2025-01-29

Permissions-Policy unload vs beforeunload is a browser-lifecycle split, not two spellings of one header: the unload feature gates the window unload event (handlers that break back-forward cache), while beforeunload is a different event for unsaved-form prompts and is not a Permissions-Policy directive. Marketing sites should disable unused device APIs and should not rely on unload for analytics beacons — use pagehide, visibilitychange, or fetch keepalive instead. RoastMyOpsec grades that a Permissions-Policy header exists on the primary document; it does not parse unload=(), fire unload, or score GPC.

### The practical difference

unload as a policy-controlled feature lets you set Permissions-Policy: unload=() (deny) or unload=self if you still have a same-origin exception. Chrome has been deprecating unload because those handlers make bfcache ineligible. Third-party tags that still listen for unload are why a document policy belongs next to your tracker diet.

beforeunload is for 'you have unsaved changes.' It is not camera, geolocation, or unload. Abusing it for analytics or to trap tab-close is hostile UX and still not a substitute for Permissions-Policy on device APIs. pagehide and visibilitychange are the usual replacements for teardown work.

### How to choose based on what you shipped

Brochure, blog, docs: ship a tight Permissions-Policy that turns off camera, microphone, geolocation, and payment unless a named route needs them. Add unload=() when you want to opt out of unload handlers immediately rather than waiting on browser deprecation. Checkout with a multi-step form may keep beforeunload on that route only.

Do not copy unload=(*) site-wide to silence a console warning from a tag manager. Remove the handler or sandbox the iframe. Feature-Policy is the old header name; send Permissions-Policy on current stacks.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Permissions-Policy unload=() | You want no unload handlers; protect bfcache | A legacy library still depends on unload | Default for brochure HTML once tags are cleaned up |
| beforeunload listener | A form with unsaved user data on that route | Site-wide trap or analytics on every page | UX prompt, not a Permissions-Policy token |
| pagehide / visibilitychange | Beacons and teardown without blocking bfcache | Treating them as clickjacking or CSP | Preferred lifecycle hooks for public sites |
| Permissions-Policy device denies | Marketing pages that never need camera or geo | Enabling payment on the blog for one checkout widget | Still the header RoastMyOpsec actually looks for |

### When beforeunload still wins

beforeunload still wins on an editor or checkout form where losing the draft is worse than a prompt. It does not win on a homepage, a blog post, or as a replacement for sendBeacon. unload=() still wins when you have retired unload listeners and want the policy to keep them retired.

Neither control replaces CSP or framing. A Permissions-Policy that only mentions unload and still allows camera is not a marketing-site policy.

### Common mistakes

The first mistake is using unload to fire analytics and then wondering why bfcache never hits.

The second mistake is treating beforeunload as a Permissions-Policy feature you can set in the header.

The third mistake is a missing Permissions-Policy entirely while arguing about unload syntax.

### What a URL roast can prove

The free header pack flags a missing Permissions-Policy. It does not parse individual directives such as unload, camera, or geolocation, does not score Global Privacy Control, and does not dispatch unload or beforeunload. Device-API tightness is still your job after the header exists. The same roast still checks CSP quality, HSTS, framing, Referrer-Policy, nosniff, HTTPS scheme, session-like cookies, paths, and public JS secrets. Header and URL read only — no exploit payloads, no HSTS preload submit.

### FAQ

**Is beforeunload the same as Permissions-Policy unload?**

No. unload in Permissions-Policy gates the unload event. beforeunload is a separate event for unsaved-changes prompts. You cannot set beforeunload in Permissions-Policy.

**Does RoastMyOpsec require unload=()?**

No. It checks that Permissions-Policy is present. It does not score the unload feature or GPC.

**Should a marketing site keep unload handlers?**

Usually no. They hurt bfcache and are being deprecated. Use pagehide or visibilitychange for beacons, and beforeunload only where a form draft matters.

---

## Partitioned Cookies vs SameSite=None for Embeds

- URL: https://roastmyopsec.com/blog/partitioned-cookies-vs-samesite-none
- Category: Safety
- Updated: 2025-01-20

Partitioned cookies vs SameSite=None is how browsers are shrinking the shared third-party jar. SameSite=None; Secure still lets an embed send one cookie on every site that frames you — a cross-site identity. The Partitioned attribute (CHIPS) stores that cookie in a jar keyed by the top-level site, so site A and site B do not share the embed's cookie. That is better for tracker-shaped embeds. It is not a reason to mark your main login cookie None+Partitioned. First-party sessions stay Lax or Strict without Partitioned. RoastMyOpsec grades Set-Cookie on the landing response. It does not load you inside third-party iframes or prove CHIPS behavior.

### The practical difference

None means 'send this cookie in third-party contexts on HTTPS.' Partitioned means 'if you send it third-party, isolate it per top-level site.' Unpartitioned None is the old advertising model. Partitioned None is the embed that still needs a cookie but should not join a global profile.

If the cookie is your wordpress_logged_in or app session, neither None nor Partitioned is the first move — keep it first-party. See SameSite=None vs Lax.

### How to choose based on the cookie's job

Login session: Lax/Strict, Host prefix, no Partitioned. Documented iframe embed that must remember a widget user: None; Secure; Partitioned; a dedicated name; still CSRF-defend state changes. Marketing pixel: prefer first-party analytics instead of a third-party cookie at all.

| Shape | Jar | When it wins | Takeaway |
| --- | --- | --- | --- |
| Lax / Strict session | First-party only (mostly) | Your site's login | Default |
| None; Secure | Shared across top-level sites | Legacy embeds you have not migrated | High CSRF and tracking cost |
| None; Secure; Partitioned | Per top-level site | Widget cookie that must exist in iframes | Better than unpartitioned None |
| Partitioned on the session cookie | Confused | Never | Don't |

### What a URL roast can see

If the landing Set-Cookie includes Partitioned, the roast can note the attribute the same way it notes SameSite. Most session cookies will not show Partitioned — that is healthy. Third-party pixels may not even be on your origin.

### Common mistakes

The first mistake is Partitioned as a substitute for Lax on the session cookie.

The second mistake is None without Secure, which browsers drop.

The third mistake is copying a CHIPS snippet onto every Set-Cookie.

### FAQ

**Do I need Partitioned on my session cookie?**

No. Session cookies should be first-party Lax or Strict. Partitioned is for third-party embed cookies.

**Is Partitioned the same as SameSite=Lax?**

No. Lax limits when the cookie is sent. Partitioned changes which jar a third-party cookie uses. Different knobs.

**Does RoastMyOpsec test CHIPS in an iframe?**

No. It reads Set-Cookie on a consented GET to your URL. Confirm embed cookies in your own staging iframe.

---

## Origin vs Referer Headers for CSRF Defense

- URL: https://roastmyopsec.com/blog/origin-vs-referer-for-csrf-defense
- Category: Safety
- Updated: 2024-12-26

Origin vs Referer for CSRF defense is a metadata choice, not a token replacement. The Origin header carries scheme-host-port without a path. The Referer (legacy spelling) can include the full URL — which is a leak — and privacy tools omit it more often. For cookie-authenticated state changes, prefer verifying Origin against an allowlist, keep SameSite=Lax or Strict, and still use CSRF tokens for defense in depth. Do not treat a missing Referer as proof of evil; some browsers send Origin on POSTs and little else. RoastMyOpsec does not submit cross-site forms. It grades Set-Cookie SameSite on the landing response. Confirm Origin checks in the app.

### The practical difference

Origin is purpose-built for 'which site started this.' Referer is a navigation leftover. Referrer-Policy that strips the Referer does not remove Origin on the requests that still send it. Checking only Referer fights your own privacy header.

SameSite reduces a class of cross-site cookie attachment. Origin checks reject unexpected callers even when SameSite is None for an embed cookie. See SameSite=None vs Lax and CSRF tokens vs SameSite.

### How to choose based on the form

Same-origin cookie POSTs: CSRF token plus Origin allowlist matching your site (and www vs apex — pick one canonical host). Cross-origin APIs with cookies: that is CORS credentials, not a Referer check. GET-only navigation should not change state, so Origin-on-GET is not your CSRF plan.

| Signal | Contains path? | Often missing? | Takeaway |
| --- | --- | --- | --- |
| Origin | No | Less often on POSTs | Prefer for CSRF allowlists |
| Referer | Yes, unless policy strips it | Yes, with strict Referrer-Policy | Do not rely on it alone |
| CSRF token | N/A | If the form omitted it | Still ship it |
| SameSite Lax/Strict | N/A | If you set None | Layer, not the only lock |

### What the roast can see

Cookie flags, not your Origin middleware. A clean SameSite grade is not proof POSTs check Origin. Pair with www vs apex so the allowlist matches the canonical host.

### Common mistakes

The first mistake is Referer-only checks plus Referrer-Policy: no-referrer.

The second mistake is allowing a null Origin as 'same-site.'

The third mistake is skipping tokens because SameSite exists.

### FAQ

**Is Origin better than Referer for CSRF?**

Yes as a site identifier. Still use CSRF tokens and SameSite. Do not use Referer as the only check.

**Does RoastMyOpsec test CSRF?**

No. No cross-site POSTs, no exploit payloads. It reads public Set-Cookie and headers.

**Should I allow a missing Origin?**

For cookie POSTs, fail closed or use another control (token). Do not treat absence as same-origin.

---

## Origin-Agent-Cluster vs COOP for Process Isolation

- URL: https://roastmyopsec.com/blog/origin-agent-cluster-vs-coop
- Category: Safety
- Updated: 2024-12-17

Origin-Agent-Cluster vs COOP for process isolation is two complementary browser isolation knobs, not substitutes. Origin-Agent-Cluster: ?1 asks supporting browsers to put this origin in its own agent cluster — a process-isolation hint so document.domain and some same-site-but-cross-origin sharing stay off. Cross-Origin-Opener-Policy (same-origin or same-origin-allow-popups) splits browsing context groups so window.opener from a cross-origin popup cannot reach you. COEP is about loading cross-origin subresources with CORP/CORS. Marketing sites that never pop windows still want framing controls (CSP frame-ancestors). RoastMyOpsec does not score Origin-Agent-Cluster, COOP, or COEP. Missing them is not an F on a brochure origin. It does grade CSP framing on the landing response.

### The practical difference

OAC is 'this origin prefers not to share a process with sibling origins on the same site.' COOP is 'this window should not share a browsing context group with its opener.' You can set OAC without COOP. Cross-origin isolation (SharedArrayBuffer, precise timers) still needs COOP+COEP together.

document.domain relaxation fights OAC. If you still use document.domain, do not expect origin-keyed clusters.

### How to choose what to ship

App origins that handle sessions: COOP same-origin or same-origin-allow-popups if you need OAuth popups; OAC ?1 if you do not rely on document.domain. Static marketing: framing + cookies first; OAC is cheap and optional. Do not set COEP require-corp until every embed is CORS or CORP-ready.

| Header | What it isolates | Breaks if | Takeaway |
| --- | --- | --- | --- |
| Origin-Agent-Cluster: ?1 | Agent cluster / process hint | You still set document.domain | Cheap on modern browsers |
| COOP same-origin | Opener / context group | You need cross-origin window.opener | See COOP vs COEP |
| COEP | Who may be loaded as a CORP resource | Third-party embeds without CORS/CORP | Not a brochure default |
| CSP frame-ancestors | Who may iframe you | You omit it and rely on OAC | Still required for clickjacking |

### What the roast can prove

Framing headers on the HTML response can surface. Absence of OAC or COOP is not scored. Confirm popups and embeds in the app. Pair with CORP vs CORS if you later chase cross-origin isolation.

### Common mistakes

The first mistake is COOP same-origin and a login popup that needs opener.

The second mistake is OAC while still assigning document.domain.

The third mistake is treating OAC as a replacement for CSP framing.

### FAQ

**Does Origin-Agent-Cluster replace COOP?**

No. OAC is origin-keyed clusters. COOP is opener and context groups. Cross-origin isolation still needs COOP+COEP.

**Should brochure sites set Origin-Agent-Cluster?**

It is a reasonable default on modern stacks. Missing it is not a roast F.

**Does RoastMyOpsec score Origin-Agent-Cluster?**

No. It does not score COOP or COEP either. Framing CSP is the header pack that is graded.

---

## MTA-STS vs DMARC for Public Domain Email

- URL: https://roastmyopsec.com/blog/mta-sts-vs-dmarc-for-email-opsec
- Category: Safety
- Updated: 2024-09-23

MTA-STS vs DMARC for public domain email is two layers that do not replace each other. DMARC (with SPF and DKIM) is how you tell receivers what to do when From alignment fails — none, quarantine, or reject. MTA-STS (RFC 8461) is a policy file at https://mta-sts.example.com/.well-known/mta-sts.txt plus a DNS TXT on _mta-sts that tells sending MTAs to use SMTP TLS to your MX and not silently fall back to cleartext. TLS-RPT is the reporting cousin. A marketing site scan of https://example.com does not see MX. RoastMyOpsec does not fetch mta-sts.txt, does not query DNS TXT, and does not score missing MTA-STS as an F.

### The practical difference

SPF/DKIM/DMARC fight spoofed From on the domain people see. MTA-STS fights opportunistic downgrade on the path to your mail servers. CAA and registrar lock are issuance and transfer controls — different jobs. See SPF vs DKIM vs DMARC.

A policy mode testing is how you roll out MTA-STS without locking out a mis-set MX. enforce is a commitment, like HSTS preload: get certificates and MX names right first.

### How to choose a policy

Every public brand domain that sends mail: SPF, DKIM, DMARC at least p=none with a rua you read, then tighten. MTA-STS: add it when you control the MX and can keep the HTTPS policy host and leaf certs healthy. Skip MTA-STS on a domain that only forwards to a mailbox you do not operate until that operator documents it. Do not publish an enforce policy that names an MX you cannot TLS.

| Control | What it covers | Failure mode | Takeaway |
| --- | --- | --- | --- |
| SPF / DKIM / DMARC | From alignment and disposition | p=reject before mail flows are clean | Do this first |
| MTA-STS | SMTP TLS to your MX | Stale policy or dead mta-sts host | Extra; not a roast F |
| TLS-RPT | Reports about TLS failures | Mailbox you ignore | Pair with MTA-STS |
| HTTPS on the website | Browser to origin | Unrelated to MX | Different layer |

### When MTA-STS wins

MTA-STS wins after DMARC is real and you operate the MX. DMARC still wins as the spoofing control. Missing MTA-STS on a brochure domain is not a roast finding. Pair with DNS hygiene if the mta-sts hostname is a dangling CNAME.

### What the roast can prove

Nothing about MX or MTA-STS. The scan is a consented GET to the URL you paste. Confirm DNS and the policy file in your mail console. Pair with security.txt if you also want a researcher inbox on the web origin.

### Common mistakes

The first mistake is MTA-STS enforce while the policy HTTPS host uses a leftover Let's Encrypt name that expired.

The second mistake is treating website HSTS as SMTP TLS.

The third mistake is skipping DMARC because MTA-STS 'sounds stronger.'

### FAQ

**Does MTA-STS replace DMARC?**

No. DMARC is From alignment. MTA-STS is SMTP TLS policy to your MX. Ship DMARC first.

**Should every marketing domain enable MTA-STS?**

Only if you control the MX and can keep the policy host healthy. Missing it is not a roast F.

**Does RoastMyOpsec check MTA-STS?**

No. It does not query MX, TXT, or /.well-known/mta-sts.txt.

---

## Mixed Content vs HTTPS for Marketing Sites

- URL: https://roastmyopsec.com/blog/mixed-content-vs-https-for-marketing-sites
- Category: Safety
- Updated: 2024-09-14

Mixed content vs HTTPS is not a choice between two equivalent setups. HTTPS on the HTML document only protects that response. If the page then loads scripts, stylesheets, or other active assets over http://, a network attacker gets a second chance to inject. Passive mixed content (images, audio, video) is quieter but still trains browsers and users to ignore the lock. Marketing sites should serve every first-party asset on HTTPS and stop pasting ancient http:// URLs from CMS fields. RoastMyOpsec's deeper vault check parses landing HTML for cleartext resource URLs — it does not intercept your visitors' traffic.

### The practical difference

HTTPS is a transport property of one request. Mixed content is a document property: the page is HTTPS, a child request is not. Browsers treat active mixed content (scripts, iframes, XHR/fetch to http://) more strictly than images.

HSTS and a redirect fix the next visit to your host. They do not rewrite a hardcoded http://cdn.example/old.js inside the HTML. That is why a site can pass a TLS check and still fail mixed content.

### How to choose what to fix first

Fix active mixed content before decorative images. A cleartext script undoes the point of TLS. Then upgrade stylesheets, then media. Prefer changing the stored URL in the CMS or CDN to https:// rather than relying on a browser upgrade hint alone.

| Asset type | Risk if loaded over HTTP | First move | Takeaway |
| --- | --- | --- | --- |
| Script | Code injection on a hostile network | Upgrade or remove the tag | Active mixed content is the emergency |
| Stylesheet | UI spoofing and some injection paths | Serve CSS from HTTPS | Treat as active-adjacent |
| Image / media | Tampered pixels, weaker user trust | HTTPS CDN or self-host | Still a miss on a marketing site |
| XHR / fetch / websocket | API calls in the clear | Same-origin HTTPS or wss:// | Do not leave 'just this one widget' |

### When HTTPS-only still fails the roast

A 301 to HTTPS with HSTS is the right envelope. Mixed content is leftover furniture from HTTP-era WordPress, Webflow embeds, and email-exported HTML. Editors paste http:// because the asset 'still loads on office Wi-Fi.'

Content-Security-Policy upgrade-insecure-requests can help as a belt. It is not a substitute for deleting the http:// URLs you control.

### Common mistakes

The first mistake is checking the address bar and never viewing source for http://.

The second mistake is upgrading the apex and leaving a blog or /assets/ host on HTTP.

The third mistake is a third-party widget whose vendor still publishes an HTTP embed snippet.

### FAQ

**Does HTTPS on the page stop mixed content?**

No. HTTPS covers that document request. Child requests listed as http:// are a separate problem and must be upgraded or removed.

**Is mixed content the same as a missing HSTS header?**

No. HSTS teaches browsers to skip HTTP for your host on later visits. Mixed content is http:// URLs already written into the HTTPS page.

**How does RoastMyOpsec find mixed content?**

The vault mixed-content check parses the landing HTML for http:// script, style, and image URLs. It is a defensive read of a public page you own — not a man-in-the-middle test.

---

## Min TLS Version at the CDN vs the Origin

- URL: https://roastmyopsec.com/blog/min-tls-version-cdn-vs-origin
- Category: Safety
- Updated: 2024-09-06

Min TLS version at the CDN vs the origin is which handshake the public internet actually sees: visitors terminate TLS on the edge, so origin-only 1.2+ never helps if the CDN still offers 1.0, and a bypassed origin IP still speaks whatever you left on the origin. Prefer TLS 1.3, keep 1.2 until old clients are gone, and turn 1.0/1.1 off at the hostname people type. RoastMyOpsec's free TLS check is HTTPS scheme and trust-store acceptance on the URL you paste — it does not negotiate min version, compare CDN to origin, or score missing TLS 1.3 as an F.

### The practical difference

CDN min TLS is the product most users hit. Origin min TLS is the origin-pull and any direct-to-origin path (forgotten A records, staging hostnames, mail-admin panels on the same IP). Align both, then keep origin off public DNS if the CDN is supposed to be the only front door.

HSTS and a 301 to HTTPS still matter more for OPSEC than winning a cipher screenshot. A valid certificate on the name you advertise is the roast's TLS evidence. Mixed content and cookie Secure flags are separate findings.

### How to choose based on what you shipped

Set the CDN (or load balancer) to TLS 1.2 minimum, enable 1.3, drop 1.0/1.1. Match origin pull so the CDN is not the only grown-up in the path. Government and many enterprise baselines treat 1.2 as the floor; 1.3 is the generation to offer.

Do not disable TLS 1.2 on the edge the week a payment iframe or old WebView still needs it. Do not assume a homepage roast proved origin-only hostnames.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Min TLS 1.2+ at the CDN | Public visitors; the hostname in ads and sitemaps | Origin still on 1.0 via a leaked IP | The control users actually negotiate |
| Min TLS 1.2+ at origin | Origin pull and any direct hostname you still publish | Hardening origin while the CDN still offers 1.0 | Necessary, not sufficient, if the edge is public |
| TLS 1.3 enabled | Modern clients; fewer handshake round trips | 1.3-only while a partner still needs 1.2 | Offer it; do not make it a roast requirement |
| HTTPS URL + trusted cert | This visit works | Treating it as a Qualys cipher audit | What the free roast actually checks |

### When origin TLS still wins the argument

Origin TLS still wins when employees, cron jobs, or a support vendor hit the origin hostname, or when a cached DNS answer bypasses the CDN. CDN TLS still wins for the campaign URL. You want both floors, plus HSTS on the public name.

A URL roast of https://www.example.com is an edge story. It will not SSH to origin or enumerate ciphers. Pair with HSTS vs redirect and do not expose origin IPs in public DNS if that is the design.

### Common mistakes

The first mistake is TLS 1.2-only on origin and TLS 1.0 still on the CDN 'for old browsers.'

The second mistake is publishing the origin A record next to the CDN CNAME.

The third mistake is reading a RoastMyOpsec TLS pass as a min-version certificate.

### What a URL roast can prove

RoastMyOpsec confirms the audited URL is HTTPS (or redirects to HTTPS) and that the runtime trust store accepts the certificate. It does not set or read a min TLS version at CDN or origin, does not list ciphers, and does not score TLS 1.3 as required. HSTS is a separate header check (missing or short max-age). The scan does not submit HSTS preload, does not score GPC, and does not run protocol-downgrade exploits. Align edge and origin in your provider consoles, then re-scan the public hostname.

### FAQ

**Is locking TLS on the origin enough?**

No. Users handshake with the CDN (or other edge). Set min TLS there, and still lock origin so a bypass is not a time machine.

**Does RoastMyOpsec check TLS 1.0 at the CDN?**

No. The free TLS check is scheme and certificate trust on the URL you submit, not a version or cipher scan.

**Should we disable TLS 1.2?**

Not by default in 2026. Prefer 1.3 and keep 1.2 until you know the clients. Disable 1.0 and 1.1 at the public edge.

---

## localStorage vs HttpOnly Cookies for Session Tokens

- URL: https://roastmyopsec.com/blog/localstorage-vs-httponly-cookies-for-sessions
- Category: Safety
- Updated: 2024-08-28

localStorage vs HttpOnly cookies for session tokens is not a framework fashion contest. localStorage (and document.cookie without HttpOnly) is readable to any script that runs on the page — including XSS and a sloppy third-party tag. HttpOnly cookies keep the browser from handing the session to JavaScript. SPAs can still use cookie-based sessions with SameSite, CSRF defenses, and a boring API. Access tokens in localStorage are convenient and a classic OPSEC miss. RoastMyOpsec grades Set-Cookie flags on the landing response and scans public JS for secret-like strings. It does not log into your app.

### The practical difference

localStorage is a JavaScript disk. XSS becomes session theft with one document.read. An HttpOnly cookie is a credential the page script cannot copy, though XSS can still drive the victim's browser (CSRF-shaped actions) — which is why you still want CSP and CSRF controls.

sessionStorage is the same class of problem, just shorter-lived. IndexedDB is still JS-visible.

### How to choose based on the app

Cookie-session apps: Secure, HttpOnly, SameSite=Lax or Strict, plus CSRF tokens or Origin checks. Token apps that 'must' use Authorization headers can keep short-lived access tokens in memory and refresh via an HttpOnly cookie — not a JWT in localStorage 'because Redux wanted it.'

| Store | JS can read? | Sent automatically? | Takeaway |
| --- | --- | --- | --- |
| HttpOnly cookie | No | Yes, by the browser | Default for session IDs |
| localStorage JWT | Yes | No — you attach it | XSS reads the vault |
| Memory only | Yes, until reload | No | Better than disk; still XSS-visible |
| document.cookie without HttpOnly | Yes | Yes | Worst of both |

### When localStorage still shows up

Tutorials put JWTs in localStorage because it is easy to demo. Production inherits the demo. Pair this with the public-JS-secrets guide: a token in localStorage is not 'hidden' any more than an API key in main.js.

If you already shipped it, rotate, move the session to HttpOnly, and keep CSP tight so the next XSS is less of a full account dump.

### Common mistakes

The first mistake is 'we have CSP so localStorage is fine.'

The second mistake is HttpOnly on the cookie and a duplicate JWT in localStorage for the SPA.

The third mistake is localStorage for the access token and no refresh rotation.

### FAQ

**Is putting a JWT in localStorage a vulnerability?**

It is a high-cost design. Any XSS can steal it. Prefer HttpOnly cookies or in-memory tokens with a cookie refresh.

**Do HttpOnly cookies break my SPA?**

No. The SPA calls same-origin APIs; the browser attaches the cookie. You add CSRF controls instead of reading the token in JS.

**Will a URL scan see localStorage?**

No. localStorage is in the browser. RoastMyOpsec sees Set-Cookie on the response and secret-like strings in public JS — not your users' disks.

---

## iframe sandbox vs CSP frame-ancestors

- URL: https://roastmyopsec.com/blog/iframe-sandbox-vs-csp-frame-ancestors
- Category: Safety
- Updated: 2024-06-04

iframe sandbox vs CSP frame-ancestors is two directions of iframe policy. sandbox on your <iframe> limits what that child document may do (scripts, forms, top-navigation, same-origin access). CSP frame-ancestors — and X-Frame-Options — limit which sites may put your page in a frame. Sandbox does not stop someone else from framing you. frame-ancestors does not sandbox a widget you load from a CDN. Marketing sites that embed third-party HTML should sandbox it. Sites that must not be clickjacked need frame-ancestors. RoastMyOpsec grades framing headers on the landing HTML. It does not parse iframe sandbox flags, does not load child frames as an attack, and does not exploit clickjacking.

### The practical difference

You control sandbox on tags you write. You control frame-ancestors on responses you send. A sandboxed ad iframe can still be a privacy problem; it is not a substitute for Permissions-Policy on camera and mic. allow-scripts + allow-same-origin together weaken sandbox — treat that combination as almost unsandboxed.

COOP and Origin-Agent-Cluster are process/opener isolation, not framing. See COOP vs COEP and OAC vs COOP.

### How to choose a control

Your page must not be framed: frame-ancestors 'none' or 'self' (plus XFO as a belt). You embed untrusted HTML: sandbox without allow-same-origin unless you have a documented exception. You embed a trusted first-party widget: still prefer a tight sandbox and a CSP on the child origin.

| Control | Direction | Typical miss | Takeaway |
| --- | --- | --- | --- |
| iframe sandbox | Parent restricts child | allow-scripts + allow-same-origin | For embeds you include |
| CSP frame-ancestors | You restrict parents | Missing on the account origin | Clickjacking control |
| X-Frame-Options | You restrict parents (legacy) | Using it instead of CSP forever | Belt; see CSP vs XFO |
| COOP | Opener / context group | Treating it as framing | Different header |

### What the roast can prove

Missing or weak framing headers on the landing response can surface. A sandbox attribute on a widget in the HTML is not scored. Confirm embeds in your templates. Pair with third-party trackers if the iframe is analytics.

### Common mistakes

The first mistake is sandboxing ads and omitting frame-ancestors on /login.

The second mistake is allow-top-navigation on a sandboxed frame that you do not fully trust.

The third mistake is treating sandbox as a CSP for the parent page.

### FAQ

**Does sandbox stop clickjacking of my site?**

No. Sandbox is for children you embed. Use frame-ancestors (and XFO) so others cannot frame you.

**Is allow-scripts + allow-same-origin safe?**

Together they largely undo sandbox. Avoid that pair unless you understand the child origin.

**Does RoastMyOpsec parse iframe sandbox?**

No. It grades framing headers on the landing response. No clickjacking payloads.

---

## HttpOnly vs Secure vs SameSite Cookies for Session OPSEC

- URL: https://roastmyopsec.com/blog/httponly-vs-secure-vs-samesite-cookies
- Category: Safety
- Updated: 2024-05-10

HttpOnly vs Secure vs SameSite is not an either-or decision for session cookies. HttpOnly keeps the cookie out of document.cookie. Secure stops it from traveling on HTTP. SameSite limits when the browser attaches it to cross-site requests. Auth and session cookies on a public site should usually carry all three: Secure, HttpOnly, and SameSite=Lax or Strict. RoastMyOpsec grades session-like cookies on the landing Set-Cookie headers only — no login automation — so absence can be inconclusive if the cookie is issued later.

### The practical difference

These flags solve different theft paths. Missing HttpOnly means any XSS on the page can read the session. Missing Secure means a cleartext hop can leak it. Missing or None SameSite means the cookie may ride along on cross-site GET/POST depending on browser defaults and your Chrome-era exceptions.

Marketing pixels and preference cookies are not the same as session cookies. Grade the cookies that look like sessions, auth, or JWTs first. Analytics cookies can stay less privileged; they should never be your login.

### How to choose based on the cookie's job

Start with what the cookie authorizes. If it can change account state or prove login, it is a vault key and gets the hard flags. If it only stores a theme or a cart id without auth, the blast radius is smaller but Secure is still cheap on an HTTPS site.

| Cookie job | HttpOnly | Secure | SameSite | Takeaway |
| --- | --- | --- | --- | --- |
| Session / auth | Required | Required | Lax or Strict | Treat as a credential. Never readable to JS. |
| CSRF token cookie | Often false (JS must read it) | Required | Strict or Lax with server Origin checks | Different pattern — do not copy session flags blindly |
| Preferences | Optional | Required on HTTPS sites | Lax is usually enough | Do not store secrets here |
| Third-party embed | Depends on vendor | Required | None requires Secure and a real cross-site need | Avoid None unless the product actually embeds cross-site |

### When SameSite=Lax wins

Lax wins for most cookie-based web apps that need top-level GET navigations from email or search to land the user logged in. It still blocks many cross-site POSTs from attaching the cookie.

Strict wins when the app is a high-value console and you would rather make users click through your own origin than accept inbound cross-site GETs with credentials.

### When SameSite=None is a trap

None is for genuine cross-site credentialed embeds. It requires Secure. Using None 'so OAuth works' without understanding the callback is a common way to reopen CSRF-shaped problems.

If you do not have a third-party iframe that must send your session cookie, you do not need None.

### What a public scan can and cannot see

RoastMyOpsec parses Set-Cookie on the URL you submit. If the session cookie is only issued after login, the free cookie check may be inconclusive. That is honest grading, not a pass.

Fix flags in the app or at the edge, then hit a URL that actually sets the cookie — or confirm in your browser's Application panel on an account you own.

### Common mistakes

The first mistake is putting session tokens in document.cookie so frontend JS can 'just read them.'

The second mistake is Secure on production and missing it on the staging host that still uses real accounts.

The third mistake is copying a marketing-site cookie policy onto the app domain that holds sessions.

### FAQ

**Do I need HttpOnly, Secure, and SameSite on every cookie?**

You need all three on session and auth cookies. Other cookies should still be Secure on HTTPS sites. HttpOnly depends on whether JavaScript must read the value.

**Is SameSite=Lax or Strict better?**

Lax is the usual default for sites that receive logged-in traffic from external links. Strict is stronger for consoles where inbound cross-site GETs should not carry the session.

**Why did an OPSEC scan not see my auth cookie?**

Public scans only see Set-Cookie on the requested URL. Cookies issued after login are often invisible without authenticated testing. Check the response that actually establishes the session.

---

## HSTS vs HTTPS Redirects for Marketing Site Safety

- URL: https://roastmyopsec.com/blog/hsts-vs-https-redirect-for-marketing-sites
- Category: Safety
- Updated: 2024-03-28

HSTS vs HTTPS redirects is not a contest with one winner. A redirect sends this visit to HTTPS. HTTP Strict-Transport-Security tells supporting browsers to remember HTTPS for a max-age, cutting off later cleartext first hops. Marketing sites that only redirect still lose the first request and any client that ignores the redirect. Sites that only set HSTS without a working HTTPS listener strand visitors. The durable setup is: TLS on, permanent redirect, then HSTS with a long max-age once you are sure HTTPS will not be turned off.

### The practical difference

A 301 or 308 from http:// to https:// is a traffic rule. It runs when the client speaks HTTP first. Attackers on the network still get a shot at that first request unless the browser already knows to upgrade.

HSTS is a memory rule. After a browser sees Strict-Transport-Security on a trusted HTTPS response, later visits skip HTTP. includeSubDomains and preload are extra commitments, not casual flags.

### How to choose based on how ready TLS is

If any important subdomain still needs HTTP, do not set includeSubDomains. If you might roll back HTTPS next week, do not set a two-year max-age and do not submit to the preload list.

If the marketing site and www already live on HTTPS with a working certificate, turn on HSTS at the edge with a max-age you can live with (often six months to two years) and keep the redirect for clients that have never seen the header.

| Control | What it fixes | What it does not fix | Takeaway |
| --- | --- | --- | --- |
| HTTPS redirect | This request's cleartext URL | The first hop for new browsers; mixed content inside the page | Required, not sufficient |
| HSTS | Later first hops for browsers that cached the policy | Users who never received the header; non-browser clients | Required once HTTPS is stable |
| HSTS preload | First visit for browsers using the preload list | Operational mistakes — you cannot casually undo it | Only after subdomains are ready |

### When a redirect-only setup fails OPSEC

Redirect-only fails when someone types the bare host, follows an old http:// campaign link, or a proxy strips upgrades. It also fails if the HTML still loads http:// scripts or images — mixed content is a separate finding on RoastMyOpsec's deeper vault checks.

The free audit flags a cleartext or soft HTTPS story when the submitted URL or its redirect target is not HTTPS.

### Common mistakes

The first mistake is HSTS on a host that still serves a broken certificate on some regions.

The second mistake is max-age=0 left over from a test, which tells browsers to forget HSTS.

The third mistake is redirecting to HTTPS while the canonical tags, sitemaps, and ads still advertise HTTP.

### FAQ

**Is HSTS or an HTTPS redirect better?**

You want both. The redirect covers this visit. HSTS covers later visits for browsers that stored the policy.

**What HSTS max-age should a marketing site use?**

Once HTTPS is stable, a long max-age (often 15552000 seconds or more) is the usual production setting. Start shorter only while you are still proving TLS will stay on.

**Should I preload HSTS?**

Only after every subdomain you include is HTTPS-ready and you understand that removal from the preload list is slow. Most brochure sites can wait.

---

## HSTS Preload vs includeSubDomains for Public Sites

- URL: https://roastmyopsec.com/blog/hsts-preload-vs-includesubdomains
- Category: Safety
- Updated: 2024-03-20

HSTS preload vs includeSubDomains for public sites is a first-visit commitment versus a header bit on later visits. includeSubDomains tells a browser that already learned HSTS from this host to apply it to subdomains too — including www if the apex sent the header. Preload is a public list (hstspreload.org / browser source) that can skip the first HTTP hop for listed domains. Preload requires includeSubDomains and a long max-age among other rules. Removal is slow. Most brochure sites want HSTS plus a redirect, not preload. RoastMyOpsec reads Strict-Transport-Security on the landing HTTPS response. It does not score missing preload, does not submit domains to the list, and does not test every subdomain.

### The practical difference

Without includeSubDomains, HSTS on example.com does not teach the browser about shop.example.com. With it, a leftover HTTP-only tool subdomain becomes a lockout. Preload applies before the first byte to your server for browsers that ship the list — there is no 'try HTTP once' for listed names.

upgrade-insecure-requests rewrites subresources on this page. It is not preload. See UIR vs HSTS.

### How to choose a commitment

All public hostnames on the domain are HTTPS with working certs: includeSubDomains is reasonable. You still have mail.example or a vendor CNAME on HTTP: do not set includeSubDomains and do not preload. Preload only after a staging period with the preload token in the header and an inventory of every DNS name. Canonical www vs apex first so you do not preload the wrong hostname.

| Control | When it applies | Rollback | Takeaway |
| --- | --- | --- | --- |
| HSTS max-age only | After this host was seen on HTTPS | Wait out max-age | Default for most sites |
| includeSubDomains | After parent HSTS is cached | Same, plus every subdomain | Only if every name is ready |
| HSTS preload list | First visit in supporting browsers | Slow list removal | Optional; not a roast F |
| HTTPS redirect | This request | Change the redirect | Still required without preload |

### What the roast can prove

HSTS present or absent on the landing response is graded as a header. Missing preload is not a finding. Missing includeSubDomains is not automatically an F — it is a product choice. Pair with certificate expiry if max-age is heroic.

### Common mistakes

The first mistake is preloading while a blog subdomain still serves HTTP.

The second mistake is includeSubDomains because a checklist said so, with an HTTP-only staging name on the same domain.

The third mistake is treating preload as required to 'pass' a roast.

### FAQ

**Does includeSubDomains require preload?**

No. Preload requires includeSubDomains. You can ship the flag without ever submitting to the list.

**Should brochure sites preload HSTS?**

Usually no until every subdomain is HTTPS-ready and you accept slow removal. Missing preload is not a roast F.

**Does RoastMyOpsec check the preload list?**

No. It reads the HSTS header on the response you get. It does not submit domains or crawl subdomains.

---

## HSTS max-age Six Months vs Two Years

- URL: https://roastmyopsec.com/blog/hsts-max-age-six-months-vs-two-years
- Category: Safety
- Updated: 2024-03-11

HSTS max-age six months vs two years is how long supporting browsers remember to skip HTTP after they see Strict-Transport-Security on HTTPS: six months is 15552000 seconds — the bar RoastMyOpsec uses when it flags a short max-age — and two years (63072000) is a longer memory and a harder rollback. Preload lists typically want at least one year plus includeSubDomains and a preload token; that is a public-list decision, not a header beauty contest. RoastMyOpsec reads HSTS on the landing HTTPS response — it does not require two years, submit the domain to the preload list, or probe every subdomain.

### The practical difference

max-age is a cache of 'use HTTPS' for this host. includeSubDomains extends that memory to names under the registrable domain once a browser has learned HSTS from this response. preload is a Chromium (and other) list so even the first visit can skip HTTP — removal is slow.

A redirect from HTTP to HTTPS still covers this request. HSTS covers later first hops for browsers that stored the header. max-age=0 is how you tell browsers to forget, which is a rollback tool, not a production default. An expired certificate plus a long max-age is a lockout, not a trophy.

### How to choose based on what you shipped

HTTPS stable on this host, no plan to go back to HTTP: start at six months, keep the redirect, then raise max-age when you are comfortable. Two years wins when the site is a fixture and you already survive cert renewals. Do not set two years and includeSubDomains while a blog or mail name still speaks HTTP.

Preload only after an inventory of every DNS name you include, a long max-age that meets the list rules, and a staging period with the preload token. Most brochure sites never need the list.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| max-age ≈ six months (15552000) | HTTPS is stable; you still want a shorter rollback window | First-time visitors still need a redirect until they cache HSTS | Meets a common scanner floor; not preload by itself |
| max-age ≈ two years (63072000) | Long-lived public origin, cert automation is boring | Turning HTTPS off becomes a long apology | Strong memory; still not a preload submission |
| max-age under six months or missing | Active TLS rollback or a staged enable | Browsers forget quickly; first hops stay HTTP-shaped | The roast may flag short or missing HSTS |
| HSTS preload list | Every included name is HTTPS-ready and you accept slow removal | Submitting without includeSubDomains and a long max-age | Optional; RoastMyOpsec will not submit you |

### When six months still wins

Six months still wins during a CDN or origin migration, after a cert scare, or when a sibling subdomain's HTTPS story is not done. Two years still wins on a boring brochure origin with automated renewal and no HTTP leftovers.

Neither duration replaces a working redirect or a valid certificate. Preload is not a roast requirement and is not performed by the scan.

### Common mistakes

The first mistake is max-age=0 left in production after a test.

The second mistake is two years plus includeSubDomains while staging.example still serves HTTP.

The third mistake is treating missing preload as a RoastMyOpsec fail.

### What a URL roast can prove

On the audited HTTPS response, RoastMyOpsec flags missing Strict-Transport-Security or a max-age under 15552000 seconds (six months). A six-month policy can pass that check; two years is not extra credit on the score. The TLS check is HTTPS scheme and trust-store acceptance, not a cipher or min-version lab. The scan does not submit HSTS preload, does not verify includeSubDomains across your zone, and does not score GPC. Pair HSTS with a permanent HTTP-to-HTTPS redirect. Header read only — no exploit payloads.

### FAQ

**Is two-year HSTS better than six months?**

It remembers longer. It is also harder to undo. Six months is a solid production floor once HTTPS will stay on. Choose from rollback risk, not from a trophy number.

**Does RoastMyOpsec require a two-year max-age?**

No. It flags missing HSTS or max-age under six months. It does not require two years and does not submit preload.

**Does a long max-age replace the HTTPS redirect?**

No. New browsers still need a redirect (or preload) for the first visit. Keep both once TLS is stable.

---

## __Host- vs __Secure- Cookie Prefixes for Session OPSEC

- URL: https://roastmyopsec.com/blog/host-prefix-vs-secure-prefix-cookies
- Category: Safety
- Updated: 2024-02-15

__Host- vs __Secure- cookie prefixes is how you stop a weaker Set-Cookie from impersonating your session name. Browsers that honor prefixes refuse to store __Secure- unless Secure is set, and refuse __Host- unless Secure is set, Path is /, and Domain is omitted so the cookie is host-only. Plain session= with HttpOnly and SameSite is still good; prefixes add a name-level lock so a subdomain or a sloppy CDN cannot quietly replace the vault key. RoastMyOpsec grades Set-Cookie on the landing response, including prefix-shaped names. It does not log in or overwrite cookies in your browser.

### The practical difference

Cookie flags are attributes on this response. A later response on a sibling host can still try to set the same cookie name with weaker rules if the name is unprotected. Prefixes make the name itself a contract the browser enforces.

__Secure- is the lighter contract: HTTPS only. __Host- is the stricter one: HTTPS, host-only, path /. Most app session cookies that already live on one origin want __Host-. CDN cookies that must be shared across www and a shop host cannot use __Host-; they may use __Secure- plus a careful Domain — or, better, stop sharing the session cookie across hosts.

### How to choose based on the cookie's job

If the cookie proves login on a single origin (app.example.com or www only), name it __Host-session (or similar), set Path=/, omit Domain, and keep HttpOnly plus SameSite. If marketing must share a preference cookie across subdomains, do not reuse the session name; use a different cookie and never prefix a cross-subdomain session as __Host- — the browser will reject it.

| Name shape | Browser requires | Typical use | Takeaway |
| --- | --- | --- | --- |
| session (no prefix) | Whatever you set | Legacy apps | Flags still matter; name can be spoofed by a weaker setter |
| __Secure-session | Secure flag | HTTPS cookies that may set Domain | Better than naked names; still not host-only |
| __Host-session | Secure, Path=/, no Domain | Single-origin session | Default for new auth cookies on HTTPS |
| __Host- on a shared Domain | Rejected | Mistake | Split hosts or drop __Host- |

### What a public scan can prove

If the landing Set-Cookie already uses __Host- or __Secure-, the roast can score that name. Cookies issued only after login stay invisible, same as other session flags. Pair this with the HttpOnly / Secure / SameSite guide: prefixes do not replace those flags; they make Secure (and host scope) harder to forget.

### Common mistakes

The first mistake is __Host- plus Domain=.example.com, which browsers drop.

The second mistake is a prefix on the session and a duplicate unprefixed copy 'for the SPA.'

The third mistake is assuming a URL roast saw the login cookie when only a marketing cookie was on the homepage.

### FAQ

**Is __Host- better than __Secure-?**

For a session that should never leave one host, yes. __Host- is stricter. Use __Secure- only when you have a documented need to set Domain on an HTTPS cookie.

**Do prefixes replace HttpOnly and SameSite?**

No. Prefixes constrain Secure, Path, and Domain. Still set HttpOnly and SameSite=Lax or Strict on auth cookies.

**Will RoastMyOpsec fail me for a plain session name?**

Missing prefixes is a hardening hint, not the same as a missing Secure flag. The scan grades what Set-Cookie actually sent on the audited URL.

---

## Sec-Fetch-Site vs Origin Headers for CSRF Defense

- URL: https://roastmyopsec.com/blog/fetch-metadata-vs-origin-for-csrf
- Category: Safety
- Updated: 2023-11-13

Sec-Fetch-Site vs Origin for CSRF defense is two browser-supplied labels, not a token replacement. Sec-Fetch-Site is Fetch Metadata: same-origin, same-site, cross-site, or none (user-initiated, no referrer). Origin is scheme-host-port of the caller on many CORS and POST requests. For cookie-authenticated state changes, reject unexpected Sec-Fetch-Site values (typically allow same-origin, maybe same-site, deny cross-site), still verify Origin against an allowlist, keep SameSite, and keep CSRF tokens. Old clients and some privacy modes omit Fetch Metadata — fail closed or fall back to tokens, do not treat absence as same-origin. RoastMyOpsec does not submit cross-site POSTs and does not score Sec-Fetch-* on your server. It grades Set-Cookie SameSite on the landing response.

### The practical difference

Origin answers 'who started this?' Sec-Fetch-Site answers 'how related is that who to this URL?' same-site includes sibling hosts that share a registrable domain. same-origin is stricter. If www and api are different origins on the same site, Sec-Fetch-Site can be same-site while Origin is not your SPA origin.

Referer still leaks paths. Fetch Metadata does not include a path. Do not drop Origin checks because you added Sec-Fetch-Site.

### How to choose a policy

State-changing cookie POSTs: require Sec-Fetch-Site of same-origin (or same-site if you truly share cookies across hosts), plus Origin allowlist, plus a CSRF token. Embeds that must POST cross-site with cookies are a SameSite=None design — do not paper over that with Metadata. Navigation GETs that change state should not exist.

| Signal | Granularity | Often missing? | Takeaway |
| --- | --- | --- | --- |
| Sec-Fetch-Site | same-origin / same-site / cross-site / none | Older browsers | Reject cross-site cookie POSTs |
| Origin | Exact scheme-host-port | Some GETs; some tools | Allowlist your canonical host |
| CSRF token | Per-session secret | If the form omitted it | Still ship it |
| SameSite Lax/Strict | Cookie attachment | If you set None | Complements Metadata; not a substitute |

### What the roast can prove

A roast can show SameSite on Set-Cookie and heuristic token fields in public HTML. It cannot prove your server reads Sec-Fetch-Site. Confirm isolation in the app. Pair with Origin vs Referer and CSRF tokens vs SameSite.

### Common mistakes

The first mistake is allowing missing Sec-Fetch-Site as if the client were same-origin.

The second mistake is treating same-site as same-origin when api.example.com should not accept the marketing origin's cookies.

The third mistake is dropping CSRF tokens because Fetch Metadata landed in Chrome.

### FAQ

**Does Sec-Fetch-Site replace CSRF tokens?**

No. It is another browser signal. Tokens still help older clients and defense in depth.

**Is same-site safe for all cookie POSTs?**

Only if every host on that site is equally trusted. Prefer same-origin when the app lives on one host.

**Does RoastMyOpsec check Fetch Metadata?**

No. It does not forge cross-site forms or inspect your server's Sec-Fetch-* handling.

---

## Feature-Policy vs Permissions-Policy for Device APIs

- URL: https://roastmyopsec.com/blog/feature-policy-vs-permissions-policy
- Category: Safety
- Updated: 2023-10-18

Feature-Policy vs Permissions-Policy is the same job under a new header name. Feature-Policy was the experimental name. Permissions-Policy is the standardized header that tells the browser which powerful APIs this document (and its iframes) may use. Public marketing sites almost never need camera, microphone, geolocation, or payment — turn them off with Permissions-Policy. Shipping only Feature-Policy is a fossil; shipping neither is an open invitation for a third-party script to prompt. RoastMyOpsec's free header pack looks for Permissions-Policy on the primary HTML response. It does not prompt for devices and it does not treat a leftover Feature-Policy as a complete substitute.

### The practical difference

Syntax moved. Feature-Policy used a different grammar; Permissions-Policy uses a structured-header allowlist (for example camera=(), geolocation=()). Directives overlap but are not a copy-paste. A CDN rule that still emits Feature-Policy only may do nothing useful in current Chrome.

iframe allow= attributes are a third control. They do not replace a document-wide Permissions-Policy on the parent marketing page.

### How to choose what to ship

Set Permissions-Policy to disable unused powerful features on HTML. Keep Feature-Policy only as a temporary dual-send if you still have ancient browsers you care about — then delete it. If a checkout iframe truly needs payment, allow it on that route only, not on the homepage.

| Header | Status | What to do | Takeaway |
| --- | --- | --- | --- |
| Permissions-Policy | Current | Disable camera, mic, geo, payment unless needed | This is the one the roast grades |
| Feature-Policy | Legacy name | Do not rely on it alone | Migrate; dual-send only briefly |
| iframe allow= | Per-frame | Tighten embeds, still set document policy | Not a homepage substitute |
| Neither | Browser defaults | Third-party tags can still request APIs | Fix this week |

### What the roast can see

Presence of Permissions-Policy on the document. A Feature-Policy-only response can still look empty to that check. Pair with the device-API guide for which features to disable.

### Common mistakes

The first mistake is copying a 2018 Feature-Policy snippet and calling it done.

The second mistake is allowing geolocation=* on the brochure because a store locator might need it one day.

The third mistake is Permissions-Policy on the API host and not on www HTML.

### FAQ

**Is Feature-Policy still required?**

No. Prefer Permissions-Policy. Dual-send only if you have a documented old-browser requirement, then remove Feature-Policy.

**Does RoastMyOpsec grade Feature-Policy?**

The free header pack looks for Permissions-Policy. Confirm both at the CDN if you still emit the old name.

**Will disabling camera break my site?**

Not if the page never used it. Store locators that need geolocation should allow it on that route, not the whole marketing origin.

---

## Expect-CT vs Certificate Transparency for Public TLS

- URL: https://roastmyopsec.com/blog/expect-ct-vs-certificate-transparency
- Category: Safety
- Updated: 2023-09-06

Expect-CT vs Certificate Transparency is a leftover header versus a living ecosystem. Expect-CT told browsers to require CT evidence and optionally report failures. Chrome dropped enforcement; MDN marks the header obsolete. Shipping Expect-CT today is cargo-cult OPSEC. Certificate Transparency logs still record publicly trusted certificates. Operators should monitor new issuance for their names (CA mail, crt.sh-style search, or a vendor) and keep CAA tight. RoastMyOpsec's TLS check is HTTPS plus trust-store acceptance at scan time. It does not send Expect-CT, does not query CT logs, and does not watch expiry for you — see certificate expiry vs HSTS.

### The practical difference

Expect-CT was a browser policy header. CT is a set of append-only logs CAs submit to. You can have CT without Expect-CT — that is the current world. You cannot resurrect Expect-CT into a useful control.

A surprise certificate in a log is an inventory event. It may be a CDN, a forgotten load balancer, or something you should revoke. CAA limits who should issue; CT helps you notice who did.

### How to choose what to operate

Delete Expect-CT from the CDN template. Turn on issuance alerts at your CA or a CT monitor. Keep CAA current. Automate renewal. Do not add Expect-CT to a header checklist in 2026.

| Control | Status | Job | Takeaway |
| --- | --- | --- | --- |
| Expect-CT header | Obsolete | None in modern browsers | Remove it |
| CT logs | Current for public CAs | Public record of issuance | Monitor names you own |
| CAA | Current | Who may issue | See CAA vs registrar lock |
| HSTS + live cert | Current | This visit's transport | See expiry vs HSTS |

### What the roast can see

TLS scheme and trust at scan time. Presence of Expect-CT is not a score bonus. Absence of CT monitoring is outside the HTTP response.

### Common mistakes

The first mistake is a 2018 header pack that still requires Expect-CT.

The second mistake is ignoring a CT alert because 'we use Let's Encrypt.'

The third mistake is Expect-CT report-uri to an unattended inbox.

### FAQ

**Should I enable Expect-CT?**

No. The header is obsolete. Monitor Certificate Transparency and CAA instead.

**Does RoastMyOpsec check CT logs?**

No. The TLS check is HTTPS and trust-store acceptance. Watch issuance in your CA or a CT monitor.

**Is a CT-logged cert a vulnerability?**

Logging is normal for public CAs. A cert you did not request is the finding — inventory and revoke if needed.

---

## Document-Policy vs Permissions-Policy for Page Features

- URL: https://roastmyopsec.com/blog/document-policy-vs-permissions-policy
- Category: Safety
- Updated: 2023-07-17

Document-Policy vs Permissions-Policy for page features is two structured-header policies, not one misspelled header. Permissions-Policy (and legacy Feature-Policy) allowlists powerful APIs: camera, microphone, geolocation, payment, and related ch-* Client Hints. Document-Policy configures document-level behaviors such as force-load-at-top, unsized-media, and js-profiling — knobs for layout and profiling, not 'who may use the camera.' Marketing sites should lock unused Permissions-Policy features first. Document-Policy is optional hardening. RoastMyOpsec reads Permissions-Policy on the landing HTML as part of the free header pack. It does not score Document-Policy, does not prompt for device permissions, and does not enable js-profiling.

### The practical difference

Permissions-Policy answers 'may this origin (or iframe) use this powerful API?' Document-Policy answers 'how should this document behave for these named features?' Mixing them up is how teams paste a Document-Policy from a blog and think camera is off.

iframe allow= is a third control for children. It does not replace either header on the parent. See Feature-Policy vs Permissions-Policy for the rename.

### How to choose what to ship

Homepage: Permissions-Policy that disables unused device APIs. Checkout that needs payment: allow it on that route. Document-Policy: add force-load-at-top if you care about scroll restoration attacks on fragments; skip js-profiling in production. Do not copy an experimental policy from a lab origin onto the brand CDN without a browser matrix.

| Header | Typical job | Roast today | Takeaway |
| --- | --- | --- | --- |
| Permissions-Policy | Camera, geo, payment, many ch-* | Read on landing HTML | Lock unused APIs |
| Feature-Policy | Legacy grammar | Not a substitute | See Feature-Policy vs Permissions-Policy |
| Document-Policy | Document configuration features | Not scored | Optional; not a camera lock |
| iframe allow= | Child frame APIs | Not parsed | Complement, not a parent policy |

### What the roast can prove

Permissions-Policy on the primary response can surface in the header pack. Missing Document-Policy is not a finding. Confirm device APIs in templates. Pair with Client Hints if you also Accept-CH high-entropy fields.

### Common mistakes

The first mistake is Document-Policy with no Permissions-Policy and calling the site 'locked down.'

The second mistake is enabling js-profiling on production HTML.

The third mistake is treating Document-Policy as CSP.

### FAQ

**Does Document-Policy replace Permissions-Policy?**

No. They name different features. Disable unused device APIs with Permissions-Policy.

**Should brochure sites set Document-Policy?**

Only if you need a listed document feature. Missing it is not a roast F.

**Does RoastMyOpsec score Document-Policy?**

No. It reads Permissions-Policy on the landing response. No device prompts.

---

## X-DNS-Prefetch-Control vs Referrer-Policy for URL Leaks

- URL: https://roastmyopsec.com/blog/dns-prefetch-vs-referrer-policy
- Category: Safety
- Updated: 2023-06-21

X-DNS-Prefetch-Control vs Referrer-Policy is two different gossip networks. Referrer-Policy limits what the next site sees in the Referer header — path, query, origin, or nothing. X-DNS-Prefetch-Control (and <link rel=dns-prefetch>) decides whether the browser may resolve hostnames it has not navigated to yet, which can tell a recursive resolver that your page mentioned cdn.example.net. They do not replace each other. Public marketing sites should set Referrer-Policy (often strict-origin-when-cross-origin) on HTML. Turning prefetch off is optional hygiene, especially on pages that mention sensitive internal hostnames. RoastMyOpsec grades Referrer-Policy on the primary document response. It does not score prefetch-control today.

### The practical difference

Referrer is a hop-to-hop HTTP header. Tokens in query strings ride along unless you clamp the policy — see open redirects vs tokens in URLs. Prefetch is a DNS side channel: the browser looks up names early for speed. Corporate resolvers and some networks log those lookups.

Most brochure pages prefetch public CDNs on purpose. The OPSEC miss is prefetching staging.internal or a hostname that is not meant to be guessed from a public HTML comment.

### How to choose based on the page

Ship Referrer-Policy on every HTML document this week. Keep tokens out of URLs. Use dns-prefetch only for origins you already load as first- or third-party assets. Set X-DNS-Prefetch-Control: off if the page is an authenticated app shell that should not hint extra names — test performance, then decide.

| Control | Who learns what | Typical marketing default | Takeaway |
| --- | --- | --- | --- |
| Referrer-Policy | The next website | strict-origin-when-cross-origin | Do this; see the Referrer guide |
| X-DNS-Prefetch-Control: off | Resolvers see fewer speculative names | Often unset (browser default on) | Useful on sensitive app HTML |
| link rel=dns-prefetch | Resolver sees that hostname early | Fonts and tag managers | Only for hosts you already trust in CSP |
| HSTS / HTTPS | Transport, not these leaks | Required anyway | Different layer |

### What a header roast will not claim

A missing prefetch-control header is not an F. A missing Referrer-Policy still is a no-brainer on the free checklist. Prefetch does not fix mixed content or clickjacking.

### Common mistakes

The first mistake is prefetch-control: off as a substitute for Referrer-Policy.

The second mistake is dns-prefetch to a hostname that only exists on the corp VPN.

The third mistake is tokens in URLs plus a loose referrer policy.

### FAQ

**Is DNS prefetch a vulnerability?**

It is a privacy and inventory hint, not a bypass. Clamp it on sensitive pages. Fix Referrer-Policy and URL tokens either way.

**Does RoastMyOpsec check X-DNS-Prefetch-Control?**

The free header pack grades Referrer-Policy, not prefetch-control. Confirm prefetch at the CDN if you rely on it.

**Will turning prefetch off break my site?**

It can add a little latency on first hops to third-party hosts. It should not break rendering. Test, then keep Referrer-Policy regardless.

---

## DNS Hygiene vs DNSSEC and Registrar Lock

- URL: https://roastmyopsec.com/blog/dns-hygiene-vs-dnssec-and-registrar-lock
- Category: Safety
- Updated: 2023-06-13

DNS hygiene vs DNSSEC and registrar lock is a stack, not a single checkbox. Hygiene is knowing who your nameservers are, removing stale records, and using a registrar you can actually recover. Registrar transfer lock stops a class of domain-theft tickets. DNSSEC cryptographically authenticates DNS answers when resolvers check it. A public site can look fine on HTTPS and still be one unlocked registrar account away from a quiet hijack. RoastMyOpsec looks up public NS records for inventory. It does not attempt transfers, zone edits, or DNSSEC forging.

### The practical difference

Nameserver inventory is operational: which company answers for the zone, and whether leftover NS still point at a forgotten provider. Registrar lock is an account control: disable unauthorized transfers. DNSSEC is a protocol control: signed records so a poisoned cache is harder to believe.

You can have DNSSEC and still lose the domain if the registrar login is email-only and unlocked. You can lock the registrar and still have dangling CNAMEs.

### How to choose what to do first

Recover the registrar account (unique password, MFA). Turn on transfer lock. Inventory NS and leftover CNAMEs. Enable DNSSEC where the DNS host and TLD support it without breaking mail. Then keep a change log so marketing subdomains do not rot.

| Control | Job | Failure mode | Takeaway |
| --- | --- | --- | --- |
| Know your NS | Accountable DNS operator | Orphaned hosts, two providers fighting | Start with a written inventory |
| Registrar lock | Block surprise transfers | Unlocked domain + phished email | Do this before DNSSEC debates |
| DNSSEC | Authenticate DNS data | Mis-signed zone = outage | Enable when the host makes it boring |
| Stale records | Remove ghosts | Dangling CNAME / old MX | Pairs with the takeover guide |

### When DNSSEC waits

If the DNS panel cannot roll keys safely, or mail breaks on a bad DS record, fix operations first. An unlocked registrar with perfect DNSSEC is still a weak front door.

Preview and staging zones deserve the same lock story as production, or they become the reset path.

### Common mistakes

The first mistake is a shared registrar login in a spreadsheet.

The second mistake is enabling DNSSEC and never monitoring validation failures.

The third mistake is changing NS without updating DS records.

### FAQ

**Is DNSSEC required for a marketing site?**

It is strongly recommended where supported, but it is not a substitute for registrar MFA and transfer lock. Hygiene first, then signing.

**Does RoastMyOpsec prove DNSSEC is valid?**

No. It inventories public nameservers. Confirm DNSSEC in your DNS host and with a resolver you trust.

**What should I do before transferring DNS providers?**

Lower TTLs, copy records, switch NS, watch mail and the website, then remove the old provider. Keep registrar lock on until you intend a transfer.

---

## DANE TLSA vs CAA for Public Site Certificates

- URL: https://roastmyopsec.com/blog/dane-tlsa-vs-caa-for-public-sites
- Category: Safety
- Updated: 2023-05-01

DANE TLSA vs CAA for public site certificates is two DNS controls that people mix up. CAA (RFC 8659) tells CAs which issuers may mint a cert for the name — a policy the CA ecosystem is supposed to honor. DANE (RFC 6698) puts a TLSA record in DNSSEC that says which leaf or CA key SMTP or TLS clients should accept. Major browsers do not use DANE for HTTPS. SMTP and some mail stacks do. A marketing site still wants CAA plus automated renewal and HSTS. RoastMyOpsec confirms HTTPS and the runtime trust store. It does not query CAA or TLSA, does not validate DNSSEC, and does not score missing DANE as an F.

### The practical difference

CAA is issuance: who may create a cert. DANE is consumption: which key a DANE-aware client should trust, if DNSSEC is valid. Certificate Transparency is a public log of what was issued — see Expect-CT vs CT. Registrar lock stops someone else transferring the domain and rewriting CAA.

Publishing TLSA for HTTPS without a DANE-aware browser audience is busywork. Publishing CAA with issuewild and a CA you actually use is the usual first move.

### How to choose a policy

Every public HTTPS name: CAA for your real CA (and issuewild if you use wildcards), DNSSEC optional but useful, registrar lock on. Skip HTTPS DANE unless you operate a stack that checks it. Mail: DANE/TLSA for MX is a different product than website CAA — pair with MTA-STS. Do not copy a TLSA that pins a leaf you rotate every 60 days unless automation updates DNS in the same pipeline.

| Control | Who honors it | Public website | Takeaway |
| --- | --- | --- | --- |
| CAA | CAs at issuance | Yes — name your CA | See CAA vs registrar lock |
| DANE TLSA | DANE-aware clients (often mail) | Browsers generally no | Optional; not a roast F |
| DNSSEC | Resolvers that validate | Helps CAA/DANE authenticity | See DNS hygiene |
| HSTS + renewal | Browsers | Yes | See expiry vs HSTS |

### When DANE still wins

DANE wins for SMTP to an MX you operate with DNSSEC. CAA still wins for HTTPS issuance on the marketing host. Missing TLSA on a brochure is healthy. Pair with OCSP stapling if revocation ops are the worry, not DANE.

### What the roast can prove

Trust-store acceptance of the leaf you were served. CAA and TLSA are out of scope. Confirm records in DNS. Pair with dangling CNAMEs if a leftover name still issues certs.

### Common mistakes

The first mistake is TLSA on the website because 'DANE is stronger than CAA' while Chrome never looks.

The second mistake is CAA with no issuewild while you still order wildcard certs.

The third mistake is treating missing DANE as an HTTPS vulnerability.

### FAQ

**Does DANE replace CAA?**

No. CAA is for CAs at issuance. DANE is for clients that implement it — not typical browsers on HTTPS.

**Should brochure sites publish TLSA?**

Usually no for HTTPS. Publish CAA. Missing DANE is not a roast F.

**Does RoastMyOpsec check CAA or DANE?**

No. It does not query DNS TXT or TLSA and does not validate DNSSEC.

---

## CSRF Tokens vs SameSite for State-Changing Forms

- URL: https://roastmyopsec.com/blog/csrf-tokens-vs-samesite-for-state-changing-forms
- Category: Safety
- Updated: 2023-04-23

CSRF tokens vs SameSite is a pairing, not an either-or contest. SameSite on the session cookie reduces how often a foreign site can attach credentials to a request. Anti-CSRF tokens (or Fetch metadata / Origin checks) still belong on state-changing forms because SameSite=Lax allows some top-level GETs, cookies can be SameSite=None for embeds, and not every client is a modern browser. RoastMyOpsec's vault CSRF check is a heuristic on landing HTML — form tags and token-ish field names — not a proof that your server validates anything.

### The practical difference

SameSite is a cookie attribute. It changes when the browser includes the cookie on cross-site requests. It does nothing if the action does not need that cookie, or if the cookie is None.

A CSRF token is an unguessable value bound to the session that the server demands on POST/PUT/DELETE. Origin and Referer checks are a complementary server rule: reject state changes whose Origin is not you.

### How to choose based on the form

Read-only search GET forms are usually not CSRF-critical. Password change, transfer, delete, and email-update POSTs are. Cookie-based apps should set SameSite=Lax or Strict on the session cookie and still require a token or Origin check on those POSTs.

| Control | Stops | Does not stop | Takeaway |
| --- | --- | --- | --- |
| SameSite=Lax | Many cross-site POSTs with cookies | Some cross-site top-level GETs; None cookies | Necessary default, not sufficient alone |
| SameSite=Strict | More inbound cookie riding | Logged-out CSRF of unauthenticated actions | Best for consoles; worse for inbound links |
| Synchronizer / double-submit token | Forged POSTs that lack the secret | XSS that can read the token | Still need HttpOnly sessions and CSP |
| Origin / Referer check | Cross-site POSTs with a clear Origin | Missing Origin edge cases | Cheap extra belt on the server |

### When SameSite-only fails

SPA frameworks that put mutations on GET, or that use SameSite=None for a widget, re-open the old problem. Tokens and Origin checks are how you keep those designs honest.

If there are no forms on the landing page, a public scan may look clean. Confirm login and settings routes on a host you own.

### Common mistakes

The first mistake is SameSite=Lax on the cookie and no server check on POST.

The second mistake is a CSRF token in the page that the API never validates.

The third mistake is protecting /login and leaving /api/delete wide open.

### FAQ

**Is SameSite enough to stop CSRF?**

It is a strong reduction for cookie-based sites on modern browsers. It is not enough by itself for every cookie mode, every method, or every client. Keep tokens or Origin checks on state changes.

**Do JSON APIs need CSRF tokens?**

Cookie-authenticated JSON APIs still need a CSRF strategy (token, custom header that CORS would not send from a foreign site, or SameSite plus Origin). Bearer tokens in Authorization are a different model.

**How does RoastMyOpsec check CSRF?**

It parses public landing HTML for forms and common token field names. Heuristic only — no forged submissions.

---

## CSP worker-src vs script-src for Workers and Service Workers

- URL: https://roastmyopsec.com/blog/csp-worker-src-vs-script-src
- Category: Safety
- Updated: 2023-04-14

CSP worker-src vs script-src is two different script surfaces. script-src allowlists JavaScript that runs in the page. worker-src allowlists URLs that may be used as Worker, SharedWorker, and Service Worker scripts. If you only tighten script-src and leave worker-src to a loose default-src or a fallback, a XSS gadget can still start a worker from an origin you never meant to trust. Most brochure sites that do not register workers should set worker-src 'none'. Sites that do use a first-party service worker should name that origin — usually 'self' — not *. RoastMyOpsec reads CSP quality on the landing HTML. It does not score missing worker-src as a standalone F, does not register a service worker, and does not exploit worker scripts.

### The practical difference

A page script is script-src. A DedicatedWorker, SharedWorker, or Service Worker constructor/register call is a worker-src fetch. child-src is the older combined directive for workers and frames; modern policies split worker-src and frame-src. If worker-src is omitted, browsers fall back to child-src, then script-src, then default-src — which is why a 'tight' script-src still surprises teams that never named workers.

Service workers persist and intercept fetches. That is a product feature, not a reason to set worker-src *. Pair a first-party worker with Cache-Control on HTML and a clear unregister path on logout if the worker held authenticated caches. See service workers vs Cache-Control.

### How to choose worker-src

Marketing site with no workers: worker-src 'none' on day one, same reflex as object-src 'none'. PWA or offline shell: worker-src 'self' and keep the worker file on your origin with SRI or a hashed filename. Third-party 'analytics worker' URLs: name that origin only if you truly need it; prefer first-party analytics. Report-Only first if you are not sure whether a tag manager starts a worker.

| Directive | What it allowlists | Does not replace | Takeaway |
| --- | --- | --- | --- |
| script-src | Classic page JavaScript | Worker constructor URLs | Necessary, not sufficient |
| worker-src 'none' | No workers | script-src / object-src | Default for brochure sites |
| worker-src 'self' | First-party worker scripts | Cache-Control on HTML | PWAs and first-party SW |
| child-src | Legacy workers + frames mix | A split modern policy | Prefer worker-src + frame-src |

### When worker-src wins

worker-src wins when the page CSP looks strict but nobody named workers. script-src nonce/hash still wins for inline XSS on the document. Neither is a substitute for not shipping a service worker that caches personalized HTML as public.

### What the roast can prove

CSP presence and quality signals on the HTML response can surface. Missing worker-src is not scored as its own finding. Confirm the directive (or the fallback chain) in the header. Pair with object-src and connect-src so a worker cannot fetch the whole internet even if it starts.

### Common mistakes

The first mistake is assuming script-src covers Worker() and navigator.serviceWorker.register.

The second mistake is worker-src https: because one CDN hosts a helper file.

The third mistake is a service worker on 'self' that caches logged-in HTML as if it were a public asset.

### FAQ

**Does script-src already control service workers?**

Not reliably. Set worker-src explicitly. Fallback to script-src only happens when worker-src and child-src are both absent.

**Should a brochure site set worker-src 'none'?**

Yes if it does not register workers. Missing it is not a roast F by itself if default-src is already tight.

**Does RoastMyOpsec score worker-src?**

It grades CSP quality overall. It does not register a service worker or treat worker-src as the only CSP check.

---

## CSP vs X-Frame-Options for Clickjacking Protection

- URL: https://roastmyopsec.com/blog/csp-vs-x-frame-options-for-clickjacking
- Category: Safety
- Updated: 2023-04-06

CSP vs X-Frame-Options is a choice between a modern framing policy and a legacy header that older browsers still understand. Content-Security-Policy frame-ancestors is the stronger, more precise control for new sites. X-Frame-Options still matters as a compatibility fallback. Most public marketing sites should ship both, with CSP as the source of truth and X-Frame-Options as DENY or SAMEORIGIN to match.

### The practical difference

X-Frame-Options is a single-purpose header. DENY blocks all framing. SAMEORIGIN allows the same origin to frame the page. ALLOW-FROM exists in older docs but is not a reliable modern control.

Content-Security-Policy frame-ancestors can allow none, self, or an explicit origin list. It replaces the need for ALLOW-FROM and can live next to the rest of a CSP policy that also limits scripts, objects, and form targets.

RoastMyOpsec's free header audit reads the primary document response only — no payload injection, no fuzzing. Missing framing controls show up as a no-brainer finding because browsers forgive clickjacking more easily without them.

### How to choose based on the site

Start with how the page is used. A brochure marketing site almost never needs to be framed. An embeddable widget, status board, or partner checkout flow might. The header decision follows the product decision, not the other way around.

If the site must never appear inside another origin's iframe, set CSP frame-ancestors 'none' and X-Frame-Options DENY. If same-site tools frame dashboards, use frame-ancestors 'self' and X-Frame-Options SAMEORIGIN. If a named partner origin must frame a specific route, list that origin in frame-ancestors and do not pretend X-Frame-Options can express the same allowlist.

- Marketing homepage, blog, and docs: deny framing unless a real embed exists.
- App shells that iframe themselves: SAMEORIGIN / 'self'.
- Partner embeds: CSP allowlist on that route only; do not weaken the rest of the site.

### Side-by-side comparison

The table below is a planning tool, not a claim that one header works for every stack. Confirm what your CDN, reverse proxy, and app framework actually emit on the HTML document.

| Decision factor | X-Frame-Options | CSP frame-ancestors | Takeaway |
| --- | --- | --- | --- |
| Precision | DENY or SAMEORIGIN in practice | none, self, or origin list | CSP wins when a named partner must frame one route |
| Browser coverage | Very old browsers still read it | Modern browsers prefer it and it overrides XFO when both exist | Ship both until you know your traffic is modern |
| Scope | Framing only | Can sit beside script-src, object-src, form-action | CSP is a policy surface; XFO is a single lock |
| Misconfig risk | ALLOW-FROM is a false friend | A loose default-src can still leave other holes | Framing is one check; XSS defense is another |
| Best first move | DENY on public marketing HTML | frame-ancestors 'none' on the same responses | Match the two so CDNs do not disagree |

### When X-Frame-Options still wins

X-Frame-Options wins as a compatibility belt. Some older enterprise browsers and crawlers still honor it. It also documents intent in a header that security scanners and humans can read without parsing a full CSP.

It does not win as a substitute for CSP. If you only set XFO and skip frame-ancestors, you are betting that every relevant client still treats XFO as authoritative.

### When CSP frame-ancestors wins

CSP wins when you need an allowlist, when you already maintain a CSP, and when you want one policy language for framing plus script control. It also wins on routes that must be framed by a specific HTTPS origin without opening the rest of the site.

The tradeoff is operational: CSP is easy to break with a tag manager, a chatbot widget, or an inline style. Start report-only if you are new to CSP, but do not leave framing unprotected while you iterate on script-src.

### Common mistakes

The first mistake is setting X-Frame-Options on APIs and forgetting the HTML document. Clickjacking is a document problem.

The second mistake is allowing framing globally because one blog embed needed it. Scope the exception.

The third mistake is a CSP that never mentions frame-ancestors, then assuming default-src covers it. It does not. frame-ancestors is its own directive.

### Check your public headers

Paste the URL you own into RoastMyOpsec for a free header audit. The scan reads the primary response, grades CSP quality signals, HSTS, framing controls, Referrer-Policy, nosniff, and Permissions-Policy, and leaves exploit payloads out of the picture.

If framing is missing, fix it at the edge so every HTML route inherits the policy. Then re-scan.

### FAQ

**Is CSP or X-Frame-Options better for clickjacking?**

CSP frame-ancestors is the better modern control. Keep X-Frame-Options as a matching fallback (DENY or SAMEORIGIN) so older clients are not left open.

**Does frame-ancestors replace X-Frame-Options?**

In supporting browsers, CSP frame-ancestors is the framing authority when both are present. Shipping only one header still leaves gaps, so most public sites should send both until you know you do not need the legacy signal.

**Should a marketing site use DENY or SAMEORIGIN?**

Use DENY and frame-ancestors 'none' unless a same-origin tool actually frames the page. SAMEORIGIN is for first-party iframes, not for 'maybe someday' embeds.

**Does RoastMyOpsec exploit clickjacking?**

No. The audit is a consented read of public headers and responses. Private IPs and localhost are blocked. No exploit payloads.

---

## CSP style-src vs unsafe-inline for Public Sites

- URL: https://roastmyopsec.com/blog/csp-style-src-vs-unsafe-inline
- Category: Safety
- Updated: 2023-03-28

CSP style-src vs unsafe-inline for public sites is how you allow first-party CSS without inviting injected style tags. style-src lists which stylesheets and inline style blocks may apply. 'unsafe-inline' lets any inline CSS run, which is how a markup injection becomes a defacement or a data-exfil via attribute tricks — still not a substitute for script-src. Nonces or hashes for styles work like they do for scripts. Host allowlists still run every file on that origin. Most brochure sites keep unsafe-inline because a CMS injects style attributes; Report-Only is how you find that honestly. RoastMyOpsec reads CSP quality on the landing HTML. It does not score missing style-src as a standalone F, does not inject CSS, and does not exploit style sinks.

### The practical difference

script-src is JavaScript. style-src is CSS. object-src is plugins. A tight script-src with style-src 'unsafe-inline' is a common CMS stall. style-src-elem and style-src-attr split elements versus attributes if you need that precision.

Hashes pin exact inline blocks. Nonces need a fresh value per HTML response — do not bake a nonce into a year-old static file. See nonces vs hashes for scripts; the same failure modes apply to CSS.

### How to choose style-src

Static marketing site with hashed CSS files: style-src 'self' plus SRI on those links, no unsafe-inline. SSR: nonce on style tags you emit. Tag managers and CSS-in-JS that require inline: Report-Only until you can nonce, and still enforce frame-ancestors. Do not set style-src * or https:.

| Technique | What it allows | Failure mode | Takeaway |
| --- | --- | --- | --- |
| style-src 'self' | Same-origin stylesheets | Inline attributes still blocked | Default for hashed CSS files |
| Nonce or hash | Marked or exact inline CSS | Stale nonce on cached HTML | Better than unsafe-inline |
| 'unsafe-inline' | Any inline style | Injected CSS runs | CMS stall; Report-Only first |
| script-src nonce | Page JavaScript | Does not cover CSS | Set both directives |

### When style-src wins

style-src wins when script-src looks strict and the CMS still dumps style attributes. Trusted Types wins for DOM XSS sinks, not for CSS. Neither replaces SRI on third-party stylesheets.

### What the roast can prove

CSP presence and quality signals on the HTML response can surface. Missing style-src is not scored as its own finding. Confirm the directive in the header. Pair with SRI if styles load from a CDN.

### Common mistakes

The first mistake is copying script-src into style-src including 'unsafe-eval'.

The second mistake is style-src https: because one font host kept moving.

The third mistake is treating 'unsafe-inline' on styles as harmless because XSS is a script problem.

### FAQ

**Does script-src cover inline CSS?**

No. Set style-src. unsafe-inline on scripts and styles are separate holes.

**Should brochure sites allow unsafe-inline styles?**

Prefer not. If a CMS forces it, stay in Report-Only for styles and still enforce framing.

**Does RoastMyOpsec score style-src?**

It grades CSP quality overall. It does not inject CSS or treat style-src as the only CSP check.

---

## CSP strict-dynamic vs Nonce Host Allowlists

- URL: https://roastmyopsec.com/blog/csp-strict-dynamic-vs-nonce-allowlists
- Category: Safety
- Updated: 2023-03-20

CSP strict-dynamic vs nonce host allowlists is two ways to trust scripts after you already have a nonce or hash: the nonce marks the tags you meant to run, a host list allows every file on that origin, and 'strict-dynamic' tells supporting browsers to ignore those hosts and let a trusted script load children it creates. That eases tag managers; it also means a trusted script that builds tags from attacker-controlled URLs is now a loader. RoastMyOpsec reads CSP quality signals (missing policy, unsafe-eval, unqualified unsafe-inline) — it does not score strict-dynamic alone and does not execute XSS.

### The practical difference

A nonce allowlist without strict-dynamic is explicit: every script element you want must carry this response's nonce (or a matching hash). Extra <script src> injected by a trusted loader is blocked unless you also listed that host — which is how CDNs become a second XSS origin.

strict-dynamic, with a nonce or hash root of trust, drops host and scheme allowlists for script-src in browsers that understand it. Older browsers ignore the keyword; that is why compatibility snippets still add 'unsafe-inline' and https: behind it. Those fallbacks are ignored once strict-dynamic is understood. A static nonce on cached HTML still ruins either design.

### How to choose based on what you shipped

SSR or edge HTML you control: nonce per response, no unqualified unsafe-inline, then decide whether loaders must spawn children. If GTM or a bundler injects scripts, strict-dynamic is the usual way to keep the nonce model without listing every third-party host. If you have a tiny set of first-party files and no runtime loaders, skip strict-dynamic and list those origins or use hashes plus SRI.

Do not add strict-dynamic to a host-only policy with no nonce or hash — supporting browsers will ignore the hosts and have nothing to trust. Do not treat strict-dynamic as a reason to skip frame-ancestors.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Nonce (or hash) only | You mark every script; no runtime loaders | A child <script> from a loader is blocked | Tightest if the HTML is fully in your templates |
| Nonce + strict-dynamic | Trusted loaders must fetch more scripts | Those loaders become transitive trust | Usual strict-CSP pattern for apps with widgets |
| Host allowlist | Legacy CDNs you cannot nonce | Anything hosted on a listed origin can run | Weaker; pair with SRI, plan to graduate |
| unsafe-inline without nonce/hash | Never as a production plan | Inline XSS is in policy | Report-Only until you can drop it |

### When a host allowlist still wins

A host allowlist still wins as a temporary belt on static exports where you cannot rotate nonces, or on old browsers while you keep a nonce+strict-dynamic policy in front. It does not win as the long-term XSS story if that host also serves a compromised widget.

strict-dynamic wins when a nonce'd bootstrap must load children you do not want to enumerate. Hashes win for tiny immutable inline. Neither keyword is scored by name on a RoastMyOpsec header pass.

### Common mistakes

The first mistake is 'strict-dynamic' with no nonce or hash, which leaves modern browsers with an empty script-src trust root.

The second mistake is a CDN-cached HTML file with a frozen nonce.

The third mistake is assuming a roast 'CSP present' grade means strict-dynamic was reviewed.

### What a URL roast can prove

The free header pack looks for Content-Security-Policy, flags unsafe-eval, and flags unsafe-inline when no nonce or sha256/384/512 token is in the policy. Presence of 'strict-dynamic' is not a pass and absence is not an F. Framing is a separate check (frame-ancestors or X-Frame-Options). The roast does not hash your inline scripts, does not prove the nonce rotates, does not score GPC, and does not submit HSTS preload. Fix CSP at the edge, then re-scan the same URL.

### FAQ

**Do I need strict-dynamic if I already use nonces?**

Only if trusted scripts must load additional scripts without those tags carrying the nonce. If every script is in your HTML with the nonce, you can omit strict-dynamic.

**Does RoastMyOpsec require strict-dynamic?**

No. It does not score strict-dynamic alone. It grades CSP presence and a few quality signals such as unqualified unsafe-inline and unsafe-eval.

**Can host allowlists replace nonces?**

They allow anything on that origin, including a future XSS on a listed CDN. Prefer nonces or hashes for script-src; keep hosts only as a compatibility layer you are retiring.

---

## CSP script-src-elem vs script-src for Public Sites

- URL: https://roastmyopsec.com/blog/csp-script-src-elem-vs-script-src
- Category: Safety
- Updated: 2023-03-11

CSP script-src-elem vs script-src for public sites is a split allowlist versus the combined one. script-src-elem governs <script> elements (src and inline script blocks). script-src-attr governs event-handler attributes like onclick. script-src is the shorthand that fills both when you omit the split directives, and it can still feed worker-src fallback. A tight script-src-elem does not lock inline handlers. unsafe-inline on script-src still weakens both. RoastMyOpsec grades CSP quality overall. It does not score missing script-src-elem as a standalone F, does not compare nonces to every tag, and does not exploit XSS.

### The practical difference

Use script-src if you want one list for tags and handlers. Split into script-src-elem and script-src-attr when you can nonce or hash tags but still need to kill inline handlers. worker-src is workers, not <script>. default-src is the floor if you name neither.

GTM and CMS themes that inject onclick= need the attr half, not a looser elem half.

### How to choose a script policy

Brochure with a nonce'd loader: script-src 'nonce-...' 'strict-dynamic' (or hashes) and no unsafe-inline. Need to allow CDN <script src> but forbid onclick: script-src-elem with hosts/nonces, script-src-attr 'none'. Report-Only first. See nonces vs hashes and default-src vs named directives.

| Directive | What it allows | Does not cover | Takeaway |
| --- | --- | --- | --- |
| script-src | Tags and handlers (shorthand) | A split you did not set | Usual brochure start |
| script-src-elem | <script> elements | onclick= and friends | Pin tags; lock attrs separately |
| script-src-attr | Event-handler attributes | <script src> | Prefer 'none' |
| worker-src | Worker scripts | Page <script> | See worker-src vs script-src |

### When the split still wins

script-src-elem wins when tags can be nonce'd but leftover HTML handlers cannot. script-src still wins as one list for a simple brochure. Missing the split is not a finding if script-src is already tight. Pair with style-src vs unsafe-inline if the CMS also inlines CSS.

### What the roast can prove

CSP quality signals on the landing header can surface. Missing script-src-elem is not scored as its own finding. Confirm which half actually applies. Pair with Report-Only until enforcing will not break the theme.

### Common mistakes

The first mistake is script-src-elem 'self' with script-src still 'unsafe-inline'.

The second mistake is treating script-src-elem as coverage for onclick=.

The third mistake is splitting directives and forgetting worker-src.

### FAQ

**Does script-src-elem replace script-src?**

No. It only covers <script> elements. Handlers are script-src-attr. The shorthand script-src fills both if you omit the split.

**Should brochure sites set script-src-elem?**

Only if you need a different allowlist than handlers. A tight script-src is enough. Missing the split is not a roast F.

**Does RoastMyOpsec score script-src-elem?**

No. It grades CSP quality overall and does not treat script-src-elem as the only script check.

---

## CSP sandbox vs iframe sandbox for Public Pages

- URL: https://roastmyopsec.com/blog/csp-sandbox-vs-iframe-sandbox
- Category: Safety
- Updated: 2023-03-03

CSP sandbox vs iframe sandbox for public pages is two places the same idea can live. The sandbox attribute on your <iframe> limits what that child document may do. The CSP sandbox directive applies sandbox flags to the document that sent the header — as if the page itself were framed with those flags. Neither stops someone else from framing you; that is frame-ancestors (and X-Frame-Options). A brochure that only embeds a map or a video usually wants iframe sandbox, not a site-wide CSP sandbox that can break its own scripts. RoastMyOpsec grades framing headers on the landing HTML. It does not parse iframe sandbox flags, does not score CSP sandbox as a standalone F, and does not exploit clickjacking.

### The practical difference

iframe sandbox is a parent restricting a child you chose to include. CSP sandbox is you restricting your own document. allow-scripts plus allow-same-origin together weaken either form — treat that pair as almost unsandboxed. frame-src decides which hosts may be framed; it is not sandbox.

A CSP sandbox with no tokens is a locked-down document. That is rarely what a marketing CMS wants on the home page.

### How to choose a control

You embed untrusted HTML: iframe sandbox without allow-same-origin unless you have a documented exception. Your page must not be framed: frame-ancestors, not CSP sandbox. You serve a static untrusted preview origin: CSP sandbox on that origin can make sense. Your main brochure: skip CSP sandbox; keep scripts working and sandbox the third-party iframes.

| Control | Who it restricts | Typical miss | Takeaway |
| --- | --- | --- | --- |
| iframe sandbox | A child you include | allow-scripts + allow-same-origin | See iframe sandbox vs frame-ancestors |
| CSP sandbox | This document | Shipping it on a CMS home page | Rarely for brochures |
| frame-ancestors | Who may embed you | Missing both CSP and XFO | Clickjacking control |
| frame-src | Which hosts you may frame | frame-src * | See frame-src vs object-src |

### When CSP sandbox still wins

CSP sandbox wins on a dedicated origin that only renders untrusted HTML (previews, user content). iframe sandbox still wins for widgets on a normal marketing page. Missing CSP sandbox on a brochure is healthy. Pair with postMessage vs CORS if the embed also talks to the parent.

### What the roast can prove

Framing headers on the landing response can surface. Missing CSP sandbox is not a finding. iframe sandbox flags are not parsed. Confirm both yourself. Pair with CSP vs X-Frame-Options if clickjacking is the actual risk.

### Common mistakes

The first mistake is CSP sandbox on the home page because a generator listed every directive.

The second mistake is treating CSP sandbox as frame-ancestors.

The third mistake is allow-scripts and allow-same-origin together and calling the page sandboxed.

### FAQ

**Does CSP sandbox stop clickjacking?**

No. Use frame-ancestors (and X-Frame-Options) to control who may embed you.

**Should brochure sites set CSP sandbox?**

Usually no. Sandbox the iframes you include. Missing the CSP directive is not a roast F.

**Does RoastMyOpsec score CSP sandbox?**

No. It grades framing headers and does not parse iframe sandbox flags. No clickjacking payloads.

---

## CSP require-sri-for vs Integrity-Policy for CDN Scripts

- URL: https://roastmyopsec.com/blog/csp-require-sri-for-vs-integrity-policy
- Category: Safety
- Updated: 2023-02-22

CSP require-sri-for vs Integrity-Policy for CDN scripts is two ways to demand Subresource Integrity — one of them is a CSP leftover. require-sri-for was a Content-Security-Policy directive that asked supporting browsers to require integrity on script and/or style. It is deprecated and poorly supported. Integrity-Policy (and Integrity-Policy-Report-Only) is the dedicated header that tells supporting browsers that specified destinations — typically script — must include integrity. Neither header is the hash. The pin is still integrity= on the tag. RoastMyOpsec reads public HTML for cross-origin scripts without integrity. It does not score require-sri-for or Integrity-Policy, does not recompute hashes, and does not attack the CDN.

### The practical difference

SRI is per resource. require-sri-for tried to make that a CSP floor and lost the standards race. Integrity-Policy is the floor that browsers are actually shipping. CSP script-src hashes and nonces are a different allowlist — they name content or a nonce, not a CDN file hash.

Report-Only Integrity-Policy is telemetry. A require-sri-for line in an old CSP copy-paste is not a lock.

### How to choose a rollout

First-party bundles: serve from your origin so SRI is optional. Third-party CDNs you must keep: add integrity on every tag, then Integrity-Policy-Report-Only, then enforce when the report bucket is quiet. Do not add require-sri-for to a new policy. Do not enforce Integrity-Policy while a tag manager injects unsigned scripts.

| Control | What it demands | Status | Takeaway |
| --- | --- | --- | --- |
| integrity= on the tag | That one URL's bytes | Widely deployed | The actual pin |
| Integrity-Policy | That destination must have a pin | Current header | See Integrity-Policy vs SRI |
| require-sri-for | Same idea inside CSP | Deprecated | Do not ship on new sites |
| CSP nonce/hash | Allowed script content | Widely deployed | Complement; not a CDN hash |

### When require-sri-for still appears

It appears in old CSP generators and cargo-cult headers. Integrity-Policy plus real hashes still wins. Missing both headers is not a roast F if tags already have integrity. Pair with unpinned CDN scripts if the tags do not.

### What the roast can prove

Cross-origin scripts without integrity can surface. Missing Integrity-Policy and missing require-sri-for are not standalone findings. Confirm the headers yourself. Pair with CSP nonces vs hashes if you also lock inline script.

### Common mistakes

The first mistake is adding require-sri-for because a 2017 blog still lists it.

The second mistake is Integrity-Policy enforce while GTM still injects scripts.

The third mistake is treating either header as a replacement for integrity= on the tag.

### FAQ

**Should new sites set require-sri-for?**

No. Use SRI on tags you must load from a CDN, then Integrity-Policy when those tags are complete.

**Does Integrity-Policy replace CSP?**

No. It only demands integrity attributes. script-src, object-src, and the rest still do their jobs.

**Does RoastMyOpsec score require-sri-for?**

No. It looks for missing integrity on cross-origin scripts in public HTML. Neither require-sri-for nor Integrity-Policy is a standalone F.

---

## CSP report-uri vs Report-To for Violation Reports

- URL: https://roastmyopsec.com/blog/csp-report-uri-vs-report-to
- Category: Safety
- Updated: 2023-02-14

CSP report-uri vs Report-To is how violation telemetry leaves the browser, not how CSP blocks scripts. report-uri is a CSP directive that POSTs reports to a URL. The Reporting API uses a Report-To or Reporting-Endpoints header plus a report-to group name inside CSP. New browsers prefer the Reporting API; report-uri still covers older ones. Ship both during a rollout if you must, but the real OPSEC is an HTTPS collector you monitor — an orphan report-uri is a dead letter. RoastMyOpsec reads CSP quality signals on the primary HTML response. It does not POST fake violations to your endpoint and it does not prove the collector is up.

### The practical difference

report-uri is in-band in the CSP string. Report-To / Reporting-Endpoints is a separate header that names groups; CSP then says report-to groupname. The payload shape differs. Your collector must accept what you configured.

Report-Only without any reporting target is a silent flight recorder. Enforcing CSP without reports is a lock with no doorbell — still better than Report-Only forever, but you will not see what you broke.

### How to choose based on the stack

Start with report-uri to an HTTPS path you control while Report-Only is on. Add Reporting-Endpoints when you are ready for the modern API. Do not send reports to HTTP. Do not point report-uri at a third-party URL you do not have a DPA for — reports can include blocked URIs and document URLs.

| Mechanism | Where it lives | Browser story | Takeaway |
| --- | --- | --- | --- |
| report-uri | Inside CSP | Older + still widely parsed | Fine as a first collector |
| Report-To header | Separate header, JSON groups | Being succeeded by Reporting-Endpoints | Check current MDN for your CDNs |
| Reporting-Endpoints + report-to | Header + CSP group | Current Reporting API | Prefer for new work |
| No target | Nowhere | Violations vanish | You cannot graduate Report-Only |

### What the roast will not do

It will not flood your collector, will not parse whether the endpoint returns 204, and will not treat a missing report-uri as equal to missing CSP. Pair with report-only vs enforcing.

### Common mistakes

The first mistake is report-uri on HTTP.

The second mistake is a Reporting-Endpoints group name that does not match CSP report-to.

The third mistake is a public collector that reflects reports into an XSS sink.

### FAQ

**Is report-uri enough without Report-To?**

It is enough to learn during rollout. Plan for the Reporting API as browsers move. The policy still has to enforce.

**Does RoastMyOpsec send CSP reports?**

No. Consented header read only. Confirm the collector with your own staging traffic.

**Can reports leak secrets?**

They can include document URLs and blocked resource URLs. Keep tokens out of URLs and lock down who can read the collector.

---

## CSP Report-Only vs Enforcing CSP for Marketing Sites

- URL: https://roastmyopsec.com/blog/csp-report-only-vs-enforcing-csp
- Category: Safety
- Updated: 2023-02-05

CSP report-only vs enforcing CSP is a staging-vs-production-policy split, not two equal headers. Content-Security-Policy-Report-Only tells the browser to send violation reports and still run every script your tags injected. Content-Security-Policy (enforcing) is the lock: disallowed scripts, frames, and objects do not run. Marketing sites should start report-only, watch the report endpoint until GTM and fonts stop screaming, then enforce — including frame-ancestors even earlier. Living in Report-Only forever is a dashboard hobby, not OPSEC. RoastMyOpsec reads CSP quality signals on the primary HTML response. It does not inject XSS payloads to prove the policy.

### The practical difference

Report-Only is a flight recorder. Enforcing is the circuit breaker. You can ship both: a tight enforcing policy for framing and object-src, plus a report-only policy while you nonce scripts. Do not confuse 'we have CSP' with a Report-Only header that allows everything via default-src *.

frame-ancestors in Report-Only does not stop clickjacking. Framing belongs in the enforcing policy (and X-Frame-Options). See CSP vs X-Frame-Options.

### How to choose based on how messy the tags are

If the site is a brochure with three first-party scripts: enforce this week. If tag manager, chat, and A/B tools fight you: Report-Only with report-to, fix the noisiest hosts, then enforce script-src with nonces or hashes. object-src 'none' and base-uri 'none' can often enforce on day one.

| Mode | Blocks bad scripts? | Collects reports? | Takeaway |
| --- | --- | --- | --- |
| Report-Only only | No | Yes, if report-to/report-uri works | A start, not a finish |
| Enforcing, loose * | Barely | Maybe | Cosplay CSP |
| Enforcing + tight frame-ancestors | Yes for that directive | Optional | Do framing now even if scripts wait |
| Both headers | Enforcing subset | Report-Only for the rest | Usual rollout pattern |

### What the roast can see

Presence and quality signals of CSP on the document response — not whether your report collector is up, and not whether a nonce rotates. A Report-Only-only site can still fail the spirit of the header check if nothing is enforcing. Pair with the security headers checklist.

### Common mistakes

The first mistake is Report-Only for two years because a chat widget violated once.

The second mistake is enforcing CSP and putting the report endpoint on HTTP.

The third mistake is unsafe-inline forever 'until we have time for nonces.'

### FAQ

**Is report-only CSP enough?**

No. It does not stop XSS or clickjacking. Use it to learn, then enforce. Ship framing controls in the enforcing policy immediately.

**Can I run Report-Only and enforcing at the same time?**

Yes. Browsers apply the enforcing policy and can still report against the Report-Only policy. Use that to tighten without a big-bang outage.

**Does RoastMyOpsec send CSP violation reports?**

No. It reads response headers on a consented GET. It does not execute a violation campaign against your collector.

---

## CSP object-src vs Browser Plugins on Public Sites

- URL: https://roastmyopsec.com/blog/csp-object-src-vs-plugins
- Category: Safety
- Updated: 2023-01-28

CSP object-src vs browser plugins on public sites is a leftover plugin surface versus modern script policy. object-src limits URLs that may load in <object>, <embed>, and <applet>. Flash and Java-in-the-browser are gone from current Chrome, but default-src * still allows plugin-shaped fetches if you never set object-src. object-src 'none' is the usual first enforcing CSP directive alongside frame-ancestors and base-uri. It does not replace script-src, connect-src, or SRI. RoastMyOpsec reads CSP quality on the landing HTML. It does not score missing object-src as a standalone F, does not load plugin content, and does not exploit embeds.

### The practical difference

script-src is JavaScript. object-src is plugin/embed plugins. iframe sandbox and frame-ancestors are framing. A PDF in <embed> is an object-src question on some browsers; prefer a first-party viewer or a download link.

If default-src is 'self' and you omit object-src, object-src falls back to default-src. If default-src is missing or *, set object-src 'none' explicitly.

### How to choose a policy

Most marketing sites: object-src 'none' on day one. You must embed a PDF from your origin: object-src 'self' and still prefer <iframe> plus sandbox. You must load a vendor embed URL: name that origin, not *. Pair with Report-Only while you watch for blocked PDFs.

| Directive | Typical default | Does not replace | Takeaway |
| --- | --- | --- | --- |
| object-src 'none' | Brochure CSP | script-src | Ship it early |
| script-src nonce/hash | JS allowlist | object-src | See nonce vs hash |
| frame-ancestors | Who may iframe you | object-src | See CSP vs XFO |
| base-uri 'none' | No surprise <base> | object-src | See base-uri vs redirects |

### What the roast can prove

CSP presence and quality signals on the HTML response can surface. A missing object-src is not always scored as its own finding. Confirm the directive in the header. Pair with connect-src if the embed also fetches APIs.

### Common mistakes

The first mistake is default-src * and no object-src because 'nobody uses Flash.'

The second mistake is object-src https: for a single PDF host.

The third mistake is treating object-src as Trusted Types.

### FAQ

**Does object-src 'none' break YouTube iframes?**

Iframes are frame-src / child-src, not object-src. YouTube is a framing and script question.

**Should brochure sites set object-src 'none'?**

Yes as a default. Missing it is not always a roast F if default-src is already tight.

**Does RoastMyOpsec require object-src?**

It grades CSP quality overall. It does not load plugins or treat object-src as the only CSP check.

---

## CSP Nonces vs Hashes for Script Allowlists

- URL: https://roastmyopsec.com/blog/csp-nonces-vs-hashes-for-scripts
- Category: Safety
- Updated: 2023-01-19

CSP nonces vs hashes is an allowlist technique, not a third policy language. A nonce is a per-response random value on script-src and matching script tags — browsers run those tags, not random inline XSS. A hash (sha256-…) allows a specific inline snippet whose bytes match. Both beat 'unsafe-inline' and a domain allowlist that still runs anything hosted on a CDN you listed. Marketing sites often keep unsafe-inline because GTM wants it; that is the gap report-only is for. RoastMyOpsec reads CSP quality signals on the primary HTML response. It does not execute XSS, does not check whether the nonce is unique, and does not hash your inline scripts.

### The practical difference

Nonces fit server-rendered HTML: one value, many tags, new value every response (and never in a cached HTML page that outlives the nonce). Hashes fit tiny, stable inline bootstraps you rarely edit — change a space and the hash dies. Host lists (script-src https://cdn.example) allow every file on that origin, including a compromised widget.

strict-dynamic can let a nonce- or hash-trusted script load children. That is a product decision. It is not a reason to skip CSP entirely.

### How to choose based on the stack

SSR or edge-injected HTML: nonces, plus hashes for any truly static inline. Static export on a CDN: hashes or external files with SRI — a baked-in nonce on a year-old HTML file is a fossil. Tag managers: prefer loading GTM as a nonce'd external file, not unsafe-inline; if the vendor still demands inline, keep Report-Only until that changes, and still enforce frame-ancestors.

| Technique | What it allows | Failure mode | Takeaway |
| --- | --- | --- | --- |
| Nonce + matching tags | This response's marked scripts | Cached HTML with a stale nonce; nonce in a static file | Default for dynamic HTML |
| sha256 hash | Exact inline bytes | Any edit breaks the page | Tiny stable snippets |
| Host allowlist | Anything on that origin | One XSS on a listed CDN | Weaker; pair with SRI |
| unsafe-inline | Classic inline XSS | The policy stopped meaning it | Graduate off it |

### What a URL roast cannot prove

Seeing 'nonce-' in the header is not proof the HTML uses it or that it rotates. Seeing unsafe-inline is a strong hint the policy is still in training. Pair with report-only vs enforcing and with SRI for third-party scripts.

### Common mistakes

The first mistake is a static nonce in a CDN-cached HTML shell.

The second mistake is hashes plus a comment that changes in deploy.

The third mistake is nonce on scripts and unsafe-inline still on the same directive.

### FAQ

**Are nonces better than hashes?**

For HTML that is generated per request, nonces are usually easier. Hashes win for tiny immutable inline. Neither is a substitute for enforcing CSP.

**Does RoastMyOpsec verify my nonce?**

No. It reads the CSP header for quality signals. It does not compare nonces to script tags or run XSS tests.

**Can I keep unsafe-inline for GTM?**

That is a common stall. Prefer a nonce'd loader. If you cannot yet, stay honest in Report-Only for scripts and still enforce framing.

---

## CSP media-src vs Mixed Content for Public Video

- URL: https://roastmyopsec.com/blog/csp-media-src-vs-mixed-content
- Category: Safety
- Updated: 2023-01-11

CSP media-src vs mixed content for public video is two different media problems. Mixed content is an HTTPS page that still loads http:// video, audio, or other assets — browsers may block or padlock-warn. media-src in Content-Security-Policy lists which origins may be used as <video>, <audio>, and <track> resources, including blob: and data: if you allow them. A tight media-src does not fix http:// on the same host. img-src covers posters and stills, not the media file. RoastMyOpsec's mixed-content check parses landing HTML for http:// script, style, and image URLs. It does not score missing media-src as a standalone F, does not play every <video>, and does not exploit media sinks.

### The practical difference

Scheme is mixed content. Host is media-src. A page can be all-HTTPS and still allow media-src * — any origin's MP4, including a tracking beacon wrapped as video. img-src does not cover the media file. frame-src covers an iframe player, not a first-party <video src>.

Hero loops and CDN HLS belong on named HTTPS hosts. blob: from a same-origin MediaRecorder is a product choice; it is not a reason to set *.

### How to choose media-src

Brochure with a first-party CDN: media-src 'self' https://video.example.com (or your real host). Third-party players: if they iframe YouTube, that is frame-src, not media-src. Direct MP4 from a vendor: name that host. Mixed content: fix the URLs or use upgrade-insecure-requests, then keep media-src tight. Report-Only first if a CMS injects surprise video hosts.

| Control | What it limits | Does not replace | Takeaway |
| --- | --- | --- | --- |
| HTTPS + no http:// assets | Mixed content | media-src | See mixed content vs HTTPS |
| media-src allowlist | Which hosts may be video/audio | Mixed-content scheme | Name CDNs, not * |
| img-src | Posters and stills | media-src | See img-src vs mixed content |
| frame-src | Embedded player iframes | media-src | YouTube embed ≠ media-src |

### When media-src still wins

media-src wins when the page is already HTTPS and a CMS still loads MP4 from anywhere. Mixed-content fixes win first if the HTML still has http://. An iframe player is frame-src; do not confuse the two. Neither is SRI — media files rarely use integrity hashes the way scripts do.

### What the roast can prove

http:// image, script, and style URLs in landing HTML can surface. Missing media-src is not scored as its own finding. Confirm the CSP header yourself. Pair with img-src if the poster is the leak.

### Common mistakes

The first mistake is media-src * because one campaign video host kept moving.

The second mistake is treating mixed-content off as proof media-src is tight.

The third mistake is putting a YouTube iframe on media-src instead of frame-src.

### FAQ

**Does mixed-content protection replace media-src?**

No. Mixed content is the URL scheme. media-src is which hosts may load as video or audio.

**Does img-src cover <video> files?**

No. Posters are img-src. The media resource is media-src. An iframe player is frame-src.

**Does RoastMyOpsec score media-src?**

It parses http:// assets in HTML and grades CSP quality overall. It does not treat media-src as the only CSP check.

---

## CSP manifest-src vs Web App Manifest for Public Sites

- URL: https://roastmyopsec.com/blog/csp-manifest-src-vs-web-app-manifest
- Category: Safety
- Updated: 2023-01-02

CSP manifest-src vs a web app manifest for public sites is two different files' jobs. The manifest is a public JSON document (often /manifest.json or a link rel=manifest) that names name, icons, start_url, and display mode. It is supposed to be fetchable. manifest-src in Content-Security-Policy lists which origins may be used as that manifest — including 'self' for a first-party file. A tight manifest-src does not hide secrets you put in the JSON. package.json on the CDN is inventory, not a PWA manifest. RoastMyOpsec does not score missing manifest-src as a standalone F, does not treat a public manifest as a leak by itself, and does not fetch every icon URL in the file.

### The practical difference

The manifest is metadata. manifest-src is which host may supply it. default-src covers the manifest if you omit manifest-src. img-src covers icons the manifest points at, not the JSON. connect-src is APIs, not the manifest fetch.

A brochure without a PWA can skip the file. If you ship one, keep it first-party and keep API keys out of it.

### How to choose manifest-src

First-party PWA: manifest-src 'self'. Manifest hosted on a static CDN: name that HTTPS host, not *. No PWA: omit the link and omit the directive. Report-Only first if a theme injects a surprise manifest host. Pair with assetlinks if you also claim Android/iOS association files.

| Control | What it limits | Does not replace | Takeaway |
| --- | --- | --- | --- |
| Public /manifest.json | PWA metadata | manifest-src | Meant to be public; no secrets |
| manifest-src allowlist | Which hosts may be the manifest | Icon hosts | Name 'self' or one CDN |
| img-src | Icons and stills | manifest-src | See img-src vs mixed content |
| /package.json | npm inventory | A PWA manifest | Deny on the marketing origin |

### When a third-party manifest still appears

Some builders inject a vendor manifest URL. Prefer copying the JSON to your origin. Missing manifest-src is not a finding if default-src is already tight. Pair with web app manifest vs public metadata if the JSON itself holds internal hosts.

### What the roast can prove

CSP quality signals can surface. Missing manifest-src is not scored as its own finding. A reachable /package.json can still be inventory. Confirm the manifest link and the directive yourself.

### Common mistakes

The first mistake is manifest-src * because a theme kept moving the JSON.

The second mistake is putting API keys or staging hostnames in the public manifest.

The third mistake is treating a public manifest as the same leak as package.json.

### FAQ

**Is a public web app manifest a vulnerability?**

No. Manifests are meant to be public. Secrets inside the JSON are the miss.

**Should brochure sites set manifest-src?**

If you have CSP and a first-party manifest, 'self' is enough. Missing it is not a roast F if default-src is tight.

**Does RoastMyOpsec score manifest-src?**

No. It grades CSP quality overall and does not treat /manifest.json as a scored leak path.

---

## CSP img-src vs Mixed Content for Public Sites

- URL: https://roastmyopsec.com/blog/csp-img-src-vs-mixed-content
- Category: Safety
- Updated: 2022-12-25

CSP img-src vs mixed content for public sites is two different image problems. Mixed content is an HTTPS page that still loads http:// images, scripts, or styles — browsers may block or padlock-warn. img-src in Content-Security-Policy lists which origins may be used as images, including data: and blob:. A tight img-src does not fix http:// on the same host. upgrade-insecure-requests can rewrite http:// to https:// for some fetches; it is not an allowlist. RoastMyOpsec's mixed-content check parses the landing HTML for http:// script, style, and image URLs. It does not score missing img-src as a standalone F, does not load every img, and does not exploit image sinks.

### The practical difference

Scheme is mixed content. Host is img-src. A page can be all-HTTPS and still allow img-src * — any origin's picture, including a tracking pixel. style-src and script-src do not cover <img>. frame-src covers iframes, not images.

OG images and CDNs belong on named HTTPS hosts. data: URLs in img-src are a product choice; they are not a reason to set *.

### How to choose img-src

Brochure with a first-party CDN: img-src 'self' https://images.example.com (or your real host). Third-party avatars: name that host, not https:. Tracking pixels: prefer first-party analytics instead of a third-party img. Mixed content: fix the URLs or use upgrade-insecure-requests, then keep img-src tight. Report-Only first if a CMS injects surprise image hosts.

| Control | What it limits | Does not replace | Takeaway |
| --- | --- | --- | --- |
| HTTPS + no http:// assets | Mixed content | img-src | See mixed content vs HTTPS |
| img-src allowlist | Which hosts may be images | Mixed-content scheme | Name CDNs, not * |
| upgrade-insecure-requests | http:// → https:// rewrite | A host allowlist | See UIR vs HSTS |
| style-src | CSS | img-src | See style-src vs unsafe-inline |

### When img-src wins

img-src wins when the page is already HTTPS and a CMS still loads images from anywhere. Mixed-content fixes win first if the HTML still has http://. Neither is SRI — images rarely use integrity hashes the way scripts do.

### What the roast can prove

http:// image, script, and style URLs in landing HTML can surface. Missing img-src is not scored as its own finding. Confirm the CSP header yourself. Pair with third-party trackers if the 'image' is a pixel.

### Common mistakes

The first mistake is img-src * because one product photo host kept moving.

The second mistake is treating mixed-content off as proof img-src is tight.

The third mistake is img-src https: which allows any HTTPS origin.

### FAQ

**Does mixed-content protection replace img-src?**

No. Mixed content is the URL scheme. img-src is which hosts may load as images.

**Should brochure sites set img-src?**

Yes if you have CSP at all: 'self' plus named image CDNs. Missing it is not a roast F by itself.

**Does RoastMyOpsec score img-src?**

It parses http:// assets in HTML and grades CSP quality overall. It does not treat img-src as the only CSP check.

---

## CSP frame-src vs object-src for Embeds on Public Sites

- URL: https://roastmyopsec.com/blog/csp-frame-src-vs-object-src
- Category: Safety
- Updated: 2022-12-16

CSP frame-src vs object-src for embeds on public sites is two fetch allowlists that people mix up. frame-src lists URLs that may load in nested browsing contexts — typically <iframe>. object-src lists URLs for <object>, <embed>, and <applet>. YouTube, Vimeo, and most marketing embeds are iframes, so object-src 'none' does not block them. frame-ancestors is the opposite direction: who may put your page in an iframe. child-src is the older combined directive; prefer the split. RoastMyOpsec reads CSP quality on the landing HTML. It does not score missing frame-src as a standalone F, does not load third-party embeds, and does not exploit iframes.

### The practical difference

If the tag is iframe, think frame-src (and sandbox on the tag). If the tag is object or embed for a plugin or PDF, think object-src. script-src is still JavaScript on the parent page. Sandbox flags on the iframe are a third control — see iframe sandbox vs frame-ancestors.

A default-src of * with no frame-src still allows any iframe host. Name the hosts you actually embed, or use 'none' on brochure pages that have no embeds.

### How to choose a policy

Brochure with no embeds: frame-src 'none' and object-src 'none'. One YouTube video: frame-src https://www.youtube.com https://www.youtube-nocookie.com (or your consented host list), still object-src 'none'. PDF in <embed>: prefer a first-party viewer or a download link; if you must, object-src 'self'. Report-Only first if a tag manager injects surprise iframes.

| Directive | Typical tags | Does not replace | Takeaway |
| --- | --- | --- | --- |
| frame-src | <iframe> nested documents | frame-ancestors | Allowlist video hosts, not * |
| object-src 'none' | <object> <embed> plugins | frame-src | Day-one default |
| frame-ancestors | Who may iframe you | frame-src | See CSP vs XFO |
| iframe sandbox | That element's privileges | CSP fetch lists | Per-embed lock |

### When frame-src wins

frame-src wins when object-src is already 'none' and a CMS still injects third-party iframes. object-src still wins as the plugin default. Neither is postMessage origin checks — see postMessage vs CORS.

### What the roast can prove

CSP presence and quality signals on the HTML response can surface. Missing frame-src is not scored as its own finding. Confirm the directive in the header. Pair with third-party trackers if the 'embed' is a pixel iframe.

### Common mistakes

The first mistake is object-src 'none' and assuming YouTube is blocked.

The second mistake is frame-src https: because one partner kept changing CDNs.

The third mistake is treating frame-src as frame-ancestors.

### FAQ

**Does object-src 'none' break YouTube iframes?**

No. Iframes are frame-src. object-src is plugins and embed/object tags.

**Should brochure sites set frame-src 'none'?**

Yes if they do not embed anything. Missing it is not a roast F by itself if default-src is already tight.

**Does RoastMyOpsec score frame-src?**

It grades CSP quality overall. It does not load embeds or treat frame-src as the only CSP check.

---

## CSP frame-ancestors none vs self for Clickjacking

- URL: https://roastmyopsec.com/blog/csp-frame-ancestors-none-vs-self
- Category: Safety
- Updated: 2022-12-07

CSP frame-ancestors none vs self is who may put your document in a frame: 'none' matches X-Frame-Options DENY (no parent, including your own origin), and 'self' matches SAMEORIGIN so same-origin tools may iframe the page. Most marketing homepages, blogs, and login HTML should ship 'none' unless a first-party dashboard actually frames that route; a named partner origin is a third option X-Frame-Options cannot express well. RoastMyOpsec treats framing as present if X-Frame-Options or CSP frame-ancestors appears — it does not fail 'self', prefer 'none', or load clickjacking proofs.

### The practical difference

frame-ancestors is the modern framing control and, in supporting browsers, overrides X-Frame-Options when both are set. 'none' is the brochure default. 'self' is for same-origin admin shells, style guides, or help widgets that iframe your own HTML. Listing https://partner.example is for one route that must be embedded — not a site-wide gift.

X-Frame-Options remains the compatibility belt: DENY with 'none', SAMEORIGIN with 'self'. ALLOW-FROM is not a reliable modern control. iframe sandbox on children you include is the opposite direction and does not stop others from framing you.

### How to choose based on what you shipped

Public marketing origin with no first-party iframe of itself: frame-ancestors 'none' and X-Frame-Options DENY on HTML. App shell that frames its own routes: 'self' and SAMEORIGIN, scoped to those routes if the brochure must stay unframed. Partner embed: allowlist that origin on the embeddable path only.

Do not weaken the whole site because one status widget needed a parent. COOP is opener isolation, not clickjacking control.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| frame-ancestors 'none' | Brochure, blog, login, docs that must not be framed | Breaks a same-origin iframe you actually shipped | Default for public marketing HTML |
| frame-ancestors 'self' | First-party tools iframe this origin on purpose | Any same-origin XSS can frame you too | Match X-Frame-Options SAMEORIGIN |
| Origin allowlist | One partner must embed one route | Copying that list onto /login | Scope by path; XFO cannot say the same thing |
| Missing frame-ancestors | Never as a plan | Relying on XFO forever, or on COOP | Ship CSP framing; keep XFO as a belt |

### When frame-ancestors 'self' still wins

'self' still wins when your own origin frames dashboards, legacy admin, or a design-system preview and you would rather not split hosts. It does not win as a 'maybe we will embed later' default on a campaign landing page.

'none' still wins on login and checkout HTML even if the marketing blog is 'self'. Split the header by route at the edge. A roast that only sees the homepage cannot certify /app.

### Common mistakes

The first mistake is frame-ancestors 'self' on the marketing homepage because a forgotten CMS preview used an iframe once.

The second mistake is CSP without frame-ancestors, assuming default-src covers parents — it does not.

The third mistake is DENY in X-Frame-Options and 'self' in CSP, so modern browsers and old ones disagree.

### What a URL roast can prove

The free header check flags framing as missing when the document has neither X-Frame-Options nor a frame-ancestors directive in CSP. It does not distinguish 'none' from 'self', does not parse iframe sandbox, and does not score COOP. A clean framing signal means a control was present on that response, not that clickjacking was tested. CSP quality, HSTS, Referrer-Policy, nosniff, and Permissions-Policy are separate header findings. No exploit payloads, no GPC score, no HSTS preload submission.

### FAQ

**Should a marketing site use frame-ancestors none or self?**

Use 'none' unless a same-origin tool actually iframes the page. 'self' is for first-party frames, not a future maybe.

**Does RoastMyOpsec fail frame-ancestors 'self'?**

No. Framing is a presence check (X-Frame-Options or frame-ancestors). 'self' satisfies it. Match XFO so CDNs do not disagree.

**Does 'self' stop clickjacking from other sites?**

It stops other origins from framing you. It does not stop a same-origin page you did not intend to be a parent. Use 'none' when no parent should exist.

---

## CSP form-action vs Open Redirects on Public Sites

- URL: https://roastmyopsec.com/blog/csp-form-action-vs-open-redirects
- Category: Safety
- Updated: 2022-11-29

CSP form-action vs open redirects on public sites is two different hops. form-action in Content-Security-Policy tells supporting browsers which origins an HTML form may submit to. An open redirect is a server Location (or meta refresh) that sends the browser — and often a token in the query — to a caller-chosen URL. Restricting form-action to 'self' does not stop /redirect?next=https://evil.example. Allowlisting https://checkout.stripe.com on form-action does not make next= safe. Fix redirects with an allowlist of destinations and no secrets in the URL. RoastMyOpsec follows a bounded public redirect chain and looks at parameter names with values redacted. It does not score missing form-action and does not submit forms.

### The practical difference

form-action is a document policy for <form action>. JavaScript-driven navigations and fetch() are not the same control — see connect-src and your app code. Open redirects are origin logic. CSP will not rewrite your /out endpoint.

Password-change and login forms should post to your origin. Payment widgets that post to a PSP need that origin named. Do not use form-action *.

### How to choose a policy

Login, signup, and account forms: form-action 'self' (and the canonical host only — www vs apex). Hosted checkout: add the PSP origin you actually post to. Redirect endpoints: allowlist, not CSP. Pair with tokens-in-URLs if reset links still use query strings.

| Control | Stops | Does not stop | Takeaway |
| --- | --- | --- | --- |
| CSP form-action | Form POST to surprise origins | Server Location to a user URL | Belt for HTML forms |
| Open-redirect allowlist | Caller-chosen hops off-site | A form with action=https://evil | Fix the endpoint |
| CSRF token + SameSite | Cross-site state change | A first-party redirect bug | Still required |
| Referrer-Policy | Path leaks on navigation | The redirect itself | See URL leaks guide |

### What the roast can prove

Redirect chains that leave your public suffix can surface as findings. Missing form-action is not scored. Confirm the CSP directive on the form pages yourself. Heuristic token field names in HTML are CSRF-adjacent, not form-action.

### Common mistakes

The first mistake is form-action 'self' on the marketing site and a login form that posts to a different registrable domain with no CSP there.

The second mistake is treating form-action as a WAF for /redirect.

The third mistake is form-action https: which allows any HTTPS origin.

### FAQ

**Does form-action replace CSRF tokens?**

No. It limits where a form may submit. Tokens and SameSite still belong on cookie-authenticated POSTs.

**Should every brochure site set form-action?**

If you have forms, 'self' is a cheap default. Contact forms that post to a SaaS inbox need that origin named. Missing it is not a roast F.

**Does RoastMyOpsec score form-action?**

No. It does not submit forms. Open-redirect behavior on public hops is a separate check.

---

## CSP font-src vs Hosted Web Fonts on Public Sites

- URL: https://roastmyopsec.com/blog/csp-font-src-vs-hosted-web-fonts
- Category: Safety
- Updated: 2022-11-20

CSP font-src vs hosted web fonts on public sites is a fetch allowlist versus a privacy and supply-chain choice. font-src lists which origins may provide fonts for @font-face. style-src still controls the stylesheet that points at those files. A Google Fonts (or similar) CDN is extra DNS, cookies-on-some-networks, and a third-party that must stay in the CSP. Self-hosting WOFF2 on 'self' shrinks that blast radius. Mixed content still applies if the font URL is http://. RoastMyOpsec reads CSP quality on the landing HTML and may pattern-match common third-party hosts. It does not score missing font-src as a standalone F, does not download font files, and does not exploit @font-face.

### The practical difference

img-src is pictures. font-src is font files. style-src is CSS — including a <link> to fonts.googleapis.com. connect-src is fetch. A tight font-src with style-src https://fonts.googleapis.com still lets that stylesheet run. See style-src vs unsafe-inline and img-src vs mixed content.

Third-party font CDNs are a tracker-shaped dependency even when the page has no analytics tag. First-party analytics and first-party fonts are the same instinct.

### How to choose font-src

Brochure: self-host a subset of WOFF2, font-src 'self', no Google Fonts CSS. If a CMS still injects fonts.gstatic.com, name that origin in font-src and the CSS host in style-src — or remove the inject. Report-Only first. Do not set font-src https:.

| Control | What it allowlists | Does not replace | Takeaway |
| --- | --- | --- | --- |
| font-src 'self' | First-party font files | style-src | Default if you self-host |
| Hosted font CDN | That vendor's font origin | Privacy of the CSS request | Name it; prefer self-host |
| style-src | Stylesheets | font-src | The CSS that points at fonts |
| SRI on CSS | Exact stylesheet bytes | font-src | Pair if the CSS is third-party |

### When hosted fonts still win

A named font CDN wins when legal or a design system forbids copying files and you accept the extra origin. Self-host still wins for OPSEC on a brochure. Missing font-src is not a roast F if default-src is already tight. Pair with third-party trackers vs first-party analytics.

### What the roast can prove

CSP quality signals and known third-party hosts in HTML can surface. Missing font-src is not scored as its own finding. Confirm the directive in the header. Pair with SRI if fonts load through a third-party CSS file.

### Common mistakes

The first mistake is font-src * because one display font kept moving CDNs.

The second mistake is self-hosting files but still loading fonts.googleapis.com CSS.

The third mistake is treating Google Fonts as first-party because the brand uses it everywhere.

### FAQ

**Does style-src cover font files?**

No. The stylesheet is style-src. The .woff2 fetch is font-src.

**Should brochure sites self-host fonts?**

Usually yes for OPSEC. Missing font-src is not a roast F by itself if default-src is tight.

**Does RoastMyOpsec score font-src?**

It grades CSP quality overall and may note known third-party hosts. It does not treat font-src as the only CSP check.

---

## CSP default-src vs Named Directives for Public Sites

- URL: https://roastmyopsec.com/blog/csp-default-src-vs-named-directives
- Category: Safety
- Updated: 2022-11-12

CSP default-src vs named directives for public sites is a fallback versus an explicit fetch type. default-src sets the allowlist for fetch types you did not name — scripts, images, fonts, connect, and more, with documented exceptions (frame-ancestors, sandbox, report-uri, and a few others never fall back to default-src). script-src, img-src, connect-src, and the rest override that fallback for that type. A tight default-src 'self' with script-src 'unsafe-inline' is still an inline-script miss. RoastMyOpsec grades CSP quality overall. It does not treat missing default-src as the only CSP check, does not execute every allowed host, and does not exploit XSS.

### The practical difference

Named directives win when you need a different allowlist per type — CDN images, first-party scripts, one API host. default-src wins as the floor so you do not forget font-src and leak to *. frame-ancestors is not covered by default-src; set it on purpose. See CSP vs X-Frame-Options.

default-src * is not a policy. It is a shrug.

### How to choose a policy shape

Brochure with first-party assets: default-src 'self'; plus named img-src/font-src for CDNs you actually use; script-src with nonces or hashes, not unsafe-inline. Report-Only first. Do not copy a 40-directive generator dump you cannot explain. Pair with CSP report-only vs enforcing until the violation bucket is quiet.

| Directive | What it covers | Falls back to default-src? | Takeaway |
| --- | --- | --- | --- |
| default-src | Unnamed fetch types | It is the fallback | Set a tight floor |
| script-src | Scripts | Yes, if omitted | See nonces vs hashes |
| img-src | Images | Yes, if omitted | See img-src vs mixed content |
| frame-ancestors | Who may embed you | No | Set it explicitly |

### When named directives still win

They win whenever one type must be looser than the floor — a font CDN, an image CDN, a single API. default-src still wins as the catch-all. Missing default-src with every fetch type named is valid but easy to forget a new type. Pair with manifest-src if you ship a PWA JSON from another host.

### What the roast can prove

CSP quality signals on the landing header can surface. Missing default-src is not the only check. Confirm which types actually fall back. Pair with Report-Only until enforcing will not break the CMS.

### Common mistakes

The first mistake is default-src * because one widget kept moving hosts.

The second mistake is assuming frame-ancestors inherits from default-src.

The third mistake is a tight default-src and unsafe-inline on script-src forever.

### FAQ

**Does default-src cover clickjacking?**

No. frame-ancestors does not fall back to default-src. Set framing headers on purpose.

**Should brochure sites set default-src?**

Yes if you have CSP at all: a tight floor plus named CDNs. Missing it is not the only roast CSP check.

**Does RoastMyOpsec score default-src alone?**

No. It grades CSP quality overall. It does not treat default-src as the only directive that matters.

---

## CSP connect-src vs Public JavaScript API Keys

- URL: https://roastmyopsec.com/blog/csp-connect-src-vs-javascript-api-keys
- Category: Safety
- Updated: 2022-11-03

CSP connect-src vs public JavaScript API keys is a destination allowlist versus a credential in the browser. connect-src in Content-Security-Policy limits fetch, XHR, WebSocket, and EventSource URLs the page may open. A publishable Stripe-style key in app.js is still in the download whether connect-src is tight. Server secrets belong on the server. connect-src * is how a XSS gadget talks to the whole internet; it is not how you 'protect' a key. RoastMyOpsec looks for secret-shaped strings in public JavaScript. It does not score missing connect-src, does not execute the bundle, and does not call your APIs with stolen keys.

### The practical difference

script-src decides which scripts run. connect-src decides which origins those scripts may talk to. A first-party-only connect-src still ships every string in the JS. Rotate leaked keys; then proxy sensitive calls through your origin.

WebSockets need connect-src (and a server Origin check). CORS is a third gate for fetch reads. See WebSockets vs CORS and public JS keys vs server secrets.

### How to choose connect-src

Brochure with first-party analytics: connect-src 'self' plus that collector origin. Tag managers: you will add vendors — still do not add *. Publishable keys: restrict by dashboard allowlists (HTTP referrer, bundle ID), not by hoping CSP hides them. default-src does not replace an explicit connect-src if you set other fetch directives.

| Control | What it limits | What it does not | Takeaway |
| --- | --- | --- | --- |
| CSP connect-src | fetch / XHR / WS destinations | Strings already in the JS | Allowlist origins, not * |
| Publishable key in JS | Whoever downloaded the bundle | A CSP header | Treat as public; scope it |
| Server proxy | Who may call the real API | The browser key itself | Best for secrets |
| CORS ACAO | Which origins may read HTTP | connect-src | Different gate |

### What the roast can prove

Secret-shaped tokens in public JS can surface. Missing connect-src is not scored. Confirm the CSP on HTML yourself. Pair with JSONP vs CORS if a callback URL still embeds a key.

### Common mistakes

The first mistake is connect-src * because one analytics host kept moving.

The second mistake is putting a server secret in JS and adding connect-src 'self' as the fix.

The third mistake is connect-src https: which allows any HTTPS origin.

### FAQ

**Does connect-src hide API keys in JavaScript?**

No. The key is still in the file. Use publishable keys with dashboard restrictions or a server proxy.

**Should brochure sites set connect-src?**

Yes if you have CSP at all: 'self' plus named collectors. Missing it is not a roast F by itself.

**Does RoastMyOpsec score connect-src?**

No. It looks for secret-shaped strings in public JS. It does not execute fetch.

---

## CSP child-src vs worker-src for Public Sites

- URL: https://roastmyopsec.com/blog/csp-child-src-vs-worker-src
- Category: Safety
- Updated: 2022-10-26

CSP child-src vs worker-src for public sites is an old combined directive versus the split that replaced it. child-src used to cover nested browsing contexts (frames) and workers. Today, frame-src lists which hosts may be framed and worker-src lists Worker, SharedWorker, and Service Worker scripts. script-src can still fall back for workers if worker-src and child-src are absent. child-src is not gone from every browser, but new policies should name frame-src and worker-src instead of hoping one leftover covers both. RoastMyOpsec grades CSP quality overall. It does not score missing child-src as a standalone F, does not execute workers, and does not load child frames as an attack.

### The practical difference

Frames are frame-src (and frame-ancestors for who may embed you). Workers are worker-src. child-src is the older umbrella. object-src is plugins, not workers. sandbox is flags, not hosts.

A generator that still emits only child-src is cargo-cult, not a modern split.

### How to choose directives

New brochure CSP: frame-src for embeds you include, worker-src 'self' or 'none' if you have no workers, script-src for page JS. Do not add child-src to a new header. Migrating an old policy: copy child-src into frame-src and worker-src, then drop child-src when reports are quiet. Report-Only first.

| Directive | What it covers | Status | Takeaway |
| --- | --- | --- | --- |
| frame-src | Nested frames you include | Current | See frame-src vs object-src |
| worker-src | Worker scripts | Current | See worker-src vs script-src |
| child-src | Frames and workers (old) | Legacy combined | Do not ship on new sites |
| script-src | Page JS; worker fallback | Current | Not a frame allowlist |

### When child-src still appears

It appears in old CSP copies and some CMS plugins. worker-src plus frame-src still win. Missing child-src is healthy. Pair with iframe sandbox if the child also needs flags.

### What the roast can prove

CSP quality signals can surface. Missing child-src is not a finding. Confirm frame-src and worker-src yourself. Pair with service worker vs Cache-Control if a worker caches HTML.

### Common mistakes

The first mistake is adding child-src because a 2016 cheat sheet still lists it.

The second mistake is child-src 'self' and assuming YouTube iframes are blocked.

The third mistake is omitting worker-src and assuming script-src 'none' cannot spawn a worker later.

### FAQ

**Should new sites set child-src?**

No. Set frame-src and worker-src. Missing child-src is not a roast F.

**Does child-src replace frame-ancestors?**

No. child-src and frame-src are hosts you may frame. frame-ancestors is who may frame you.

**Does RoastMyOpsec score child-src?**

No. It grades CSP quality overall and does not treat child-src as the only worker or frame check.

---

## CSP base-uri vs Open Redirects on Public Sites

- URL: https://roastmyopsec.com/blog/csp-base-uri-vs-open-redirects
- Category: Safety
- Updated: 2022-10-17

CSP base-uri vs open redirects on public sites is two different ways URLs get rewritten. base-uri in Content-Security-Policy limits which origins may appear in a <base href> (and the fallback document URL). An injected or leftover <base> can make relative links, forms, and some script src values resolve elsewhere. That is not the same as a server Location to a caller-chosen URL. form-action still belongs on HTML POSTs. RoastMyOpsec follows a bounded public redirect chain. It does not score missing base-uri, does not inject <base> tags, and does not treat base-uri 'none' as required to pass.

### The practical difference

Relative URLs are resolved against the document base. A <base href='https://evil.example/'> on a page that still uses relative /login is a product bug CSP can blunt. An open redirect endpoint is origin code. CSP will not rewrite /redirect?next=.

base-uri 'none' or 'self' is a cheap default on marketing HTML that does not need <base>. Sites that use <base> for a CDN prefix must name that origin.

### How to choose a policy

No <base> tag: base-uri 'none' or 'self'. You rely on <base> for a static prefix: allow only that HTTPS origin. Open redirects: allowlist destinations in the app, not in CSP. Pair with form-action 'self' so forms do not follow a hostile base either.

| Control | Stops | Does not stop | Takeaway |
| --- | --- | --- | --- |
| CSP base-uri | Unexpected document base | Server Location to a user URL | Cheap on brochure HTML |
| CSP form-action | Form POST to surprise origins | JS-driven fetch() | See form-action vs redirects |
| Open-redirect allowlist | Caller-chosen hops | A <base> in the HTML | Fix the endpoint |
| Referrer-Policy | Path leaks on navigation | The base URL itself | See URL leaks guide |

### What the roast can prove

Public redirect hops can surface. Missing base-uri is not scored. Confirm the CSP string on HTML yourself. Pair with tokens in URLs if relative reset links still carry secrets.

### Common mistakes

The first mistake is base-uri * because a CMS injects <base>.

The second mistake is treating base-uri as a WAF for /out.

The third mistake is base-uri 'self' on www while canonical is the apex.

### FAQ

**Does base-uri replace open-redirect fixes?**

No. It limits <base>. Redirect endpoints still need an allowlist.

**Should brochure sites set base-uri 'none'?**

Yes if you do not use <base>. Missing it is not a roast F.

**Does RoastMyOpsec score base-uri?**

No. It does not inject base tags. Open-redirect behavior on public hops is a separate check.

---

## COOP vs COEP for Cross-Origin Isolation on Public Sites

- URL: https://roastmyopsec.com/blog/coop-vs-coep-for-cross-origin-isolation
- Category: Safety
- Updated: 2022-09-05

COOP vs COEP is not a paired mandatory stack for every public site. Cross-Origin-Opener-Policy (COOP) same-origin puts the document in its own browsing context group so window.opener and some cross-origin window tricks stop working. Cross-Origin-Embedder-Policy (COEP) require-corp (with CORP on subresources) is how you opt into cross-origin isolation for high-risk APIs — and it is how tag managers, fonts, and embeds break. Marketing sites should usually ship COOP same-origin-allow-popups or same-origin after testing popups, and treat COEP as a product decision, not a default. RoastMyOpsec's free header audit focuses on CSP, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy on the primary HTML response. COOP/COEP are extra isolation; missing them is not the same as missing HSTS.

### The practical difference

COOP is about who shares your top-level window. same-origin is the usual lock. same-origin-allow-popups is the compromise when you still open payment or OAuth windows. unsafe-none is the historical default.

COEP is about what you may embed. require-corp demands Cross-Origin-Resource-Policy (or CORS) on every cross-origin subresource. That is correct for a document that needs SharedArrayBuffer-class isolation. It is hostile to a CMS full of third-party scripts. CORP on the HTML itself (same-origin or same-site) is a smaller relative of this story — it tells other sites whether they may embed your resource.

### How to choose based on the page

Brochure homepage, blog, docs: try COOP same-origin (or allow-popups if checkout opens a window). Leave COEP off until you have a reason. App shells that need isolation: COEP + CORP on assets, plus COOP, tested in staging against every embed.

Do not copy a 'maximum isolation' snippet onto a site whose revenue is a pixel. You will spend a week on font CORS.

| Header | Job | When it wins | Takeaway |
| --- | --- | --- | --- |
| COOP same-origin | Separate browsing context; drop opener | Most public HTML that should not be window-tied | First isolation header to try |
| COOP allow-popups | Same, but OAuth/pay popups still work | Login or checkout that window.opens | Test the popup flow before enforcing |
| COEP require-corp | Cross-origin isolation / strict embeds | App surfaces that need it and control every asset | Not a marketing-site default |
| CORP on documents/assets | Who may load this resource as a subresource | APIs and static files you do not want hotlinked as embeds | Pair with COEP only when you chose isolation |

### What a RoastMyOpsec header pass means

A clean free header pack means the core browser guardrails were present on that response. It does not grade COOP/COEP. Framing is still CSP frame-ancestors / X-Frame-Options — COOP is not a clickjacking substitute. See the CSP vs X-Frame-Options guide.

### Common mistakes

The first mistake is COEP on www and a broken GTM container as the 'security win.'

The second mistake is COOP unsafe-none forever because a popup was never retested.

The third mistake is treating isolation headers as equal to CSP.

### FAQ

**Should every marketing site ship COEP?**

Usually no. COEP require-corp breaks third-party tags unless every asset sends CORP or CORS. Start with COOP if you want isolation at all.

**Does RoastMyOpsec score COOP and COEP?**

The free header checklist is CSP, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy. COOP/COEP are additional; confirm them in staging and at the CDN.

**Is COOP the same as X-Frame-Options?**

No. Framing controls who may iframe you. COOP controls browsing-context groups and opener. Ship framing either way.

---

## COOP same-origin-allow-popups vs same-origin

- URL: https://roastmyopsec.com/blog/coop-allow-popups-vs-same-origin
- Category: Safety
- Updated: 2022-08-27

COOP same-origin-allow-popups vs same-origin is whether a document you open with window.open() may stay in the same browsing context group: same-origin severs most opener ties, while same-origin-allow-popups is the checkout and OAuth compromise that can keep a reference to a COOP-less popup. unsafe-none is the historical default and opts out. RoastMyOpsec's free header list is CSP, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy — missing COOP is not an F on a brochure origin.

### The practical difference

COOP is about browsing context groups and window.opener, not about who may iframe you. Framing stays CSP frame-ancestors / X-Frame-Options. Cross-origin isolation for SharedArrayBuffer-class APIs still needs COOP same-origin plus COEP, which is why tag-heavy marketing pages should not copy a 'maximum isolation' snippet.

same-origin-allow-popups exists because payment and login popups often live on another origin with no COOP. same-origin will put those popups in a new group so opener looks closed. Test the real window.open path in staging before you tighten. noopener-allow-popups is a newer, even more isolating value for same-origin apps that must not script each other via open().

### How to choose based on what you shipped

Static brochure, blog, docs with no window.open: try COOP same-origin after you confirm no support widget opens a named window it must script. App or checkout that opens Stripe, PayPal, or an IdP popup: same-origin-allow-popups until that flow is a redirect instead of a popup.

Do not set COEP require-corp on www just because you set COOP. Do not treat COOP as clickjacking defense.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| same-origin | No popup scripting; you want opener severed | OAuth or pay window.open looks closed | Tighter default once popups are gone or are redirects |
| same-origin-allow-popups | You still window.open a COOP-less payment or IdP page | Leaving it forever after you moved to redirects | The usual public-app compromise |
| unsafe-none | Legacy opt-out while you inventory popups | Treating it as 'we are isolated' | Historical default; not a security win |
| COEP require-corp | You truly need cross-origin isolation and control every asset | Fonts, GTM, and embeds without CORP/CORS | Not a COOP substitute; not a brochure default |

### When same-origin-allow-popups still wins

allow-popups still wins for any origin that opens a trusted cross-origin window and must keep opener until that child sets its own COOP. same-origin still wins on locked-down app shells and on marketing HTML that never calls window.open.

If the popup is only a new tab the user interacts with alone, you may not need opener at all — use rel=noopener on navigations and test. Framing headers remain mandatory either way.

### Common mistakes

The first mistake is COOP same-origin the night before a popup checkout and a silent payment failure.

The second mistake is COOP as a replacement for frame-ancestors.

The third mistake is assuming a clean RoastMyOpsec header pack graded COOP.

### What a URL roast can prove

RoastMyOpsec does not score Cross-Origin-Opener-Policy or COEP. A clean free header result means CSP quality signals, HSTS, framing, Referrer-Policy, nosniff, and Permissions-Policy were present on that HTML response. TLS is HTTPS scheme, cookies are Secure/HttpOnly/SameSite on session-like names, plus path and JS-secret checks. Missing COOP is not a fail. The scan does not open payment popups, does not score GPC, does not submit HSTS preload, and does not send exploit payloads. Confirm window.open flows in your own staging.

### FAQ

**Should a marketing site use COOP same-origin or allow-popups?**

If you never window.open a partner, same-origin is the tighter default after a staging pass. If checkout or login still opens a window, use same-origin-allow-popups until that flow is a top-level redirect.

**Does RoastMyOpsec grade COOP?**

No. The free header checklist does not include COOP or COEP. Framing is still CSP frame-ancestors or X-Frame-Options.

**Does allow-popups weaken clickjacking protection?**

No. COOP is not a framing control. Keep frame-ancestors (and XFO) either way.

---

## Cookie Max-Age vs Session Cookies for Login Lifetime

- URL: https://roastmyopsec.com/blog/cookie-max-age-vs-session-cookies
- Category: Safety
- Updated: 2022-08-19

Cookie Max-Age vs session cookies is how long the browser is asked to keep the cookie, not how well it is locked: omit Max-Age and Expires for a session cookie, or set Max-Age in seconds when you want a clock, knowing Chromium session restore can still revive a 'session' cookie after a restart. Auth cookies still need Secure, HttpOnly, and SameSite on the name that proves login. RoastMyOpsec grades those three flags on session-like Set-Cookie names on the landing response and does not fail a site for Max-Age versus a session cookie.

### The practical difference

A session cookie is a Set-Cookie with no Max-Age and no Expires. The user agent keeps it until it considers the browsing session over. That is a browser policy, not a server-enforced logout, and session restore can make the cookie outlive 'I quit the app.'

Max-Age is a delta in seconds from when the cookie is stored. Expires is an absolute date. If both are present, Max-Age wins. Zero or negative Max-Age is how you delete a cookie you already set, matching name, Path, and Domain. Neither attribute replaces HttpOnly, Secure, or SameSite, and neither stops a stolen cookie from working until the server session is revoked.

### How to choose based on what you shipped

Pick lifetime from the product, then lock the cookie. Short-lived auth with refresh (minutes to a day of Max-Age) fits most public logins. A session cookie fits 'stay signed in only while this browser is open' — if you accept restore. Preference cookies can live longer; they should not be the login.

Do not use a year-long Max-Age on a session-shaped name to skip logout design. Server-side expiry and rotation still matter. Prefixes (__Host- / __Secure-) and Path=/ are separate contracts.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Session cookie (no Max-Age/Expires) | You want the jar empty after a real browser quit | Session restore can keep it; not a security flag | Fine for 'this sitting' if the server still expires sessions |
| Max-Age in seconds | You want a known clock and easier clock-skew behavior than Expires | Huge Max-Age is a stolen-cookie window, not a feature | Default for new auth cookies with refresh |
| Expires date | Legacy stacks that only speak Expires | Server/client clock skew; Max-Age overrides it if both are set | Prefer Max-Age when you can |
| Max-Age=0 on logout | You must drop this name now | Path/Domain mismatch leaves a twin cookie alive | Match the original setter; still revoke the server session |

### When a session cookie still wins

A session cookie still wins on a kiosk-style or shared-device flow where you do not want a calendar reminder sitting in the jar after quit — as long as you also expire the server session and you accept restore. It does not win as a substitute for HttpOnly on an XSS-prone marketing origin.

Persistent Max-Age wins when users expect to come back tomorrow without a full login, or when you rotate refresh tokens on a clock. Keep the lifetime honest in your privacy copy. A public roast of the homepage often never sees the login Set-Cookie at all.

### Common mistakes

The first mistake is treating a session cookie as automatically safer than Max-Age.

The second mistake is a 400-day auth Max-Age because the browser cap exists, not because the account should live that long in the jar.

The third mistake is deleting with Max-Age=0 on Path=/ while the original cookie was Path=/app.

### What a URL roast can prove

Paste a URL you own into RoastMyOpsec for a free read of landing headers, HTTPS scheme, session-like cookie flags, sensitive paths, and public JavaScript secrets. Cookie grading is Secure, HttpOnly, and SameSite on names that look like session, auth, token, jwt, or sid — not Max-Age versus session as a standalone F, not cookie Priority, and not a login walkthrough. No Set-Cookie on the homepage is an info result, not a pass on logout hygiene. The scan does not score GPC, does not submit HSTS preload, and does not run exploit payloads.

### FAQ

**Is a session cookie more secure than Max-Age?**

No. Lifetime and flags are different jobs. Session cookies can persist across restore. Set Secure, HttpOnly, and SameSite either way, and expire the server session on logout.

**Does RoastMyOpsec fail missing Max-Age on auth cookies?**

No. Max-Age versus a session cookie is not a standalone fail. The free cookie check looks for Secure, HttpOnly, and SameSite on session-like names on the audited response.

**Should login cookies use Max-Age or stay session cookies?**

Use Max-Age when you want a known lifetime and refresh. Use a session cookie when you truly want browser-session semantics and accept restore. Do not pick either to skip HttpOnly.

---

## Clear-Site-Data vs Cookie Expiry on Logout

- URL: https://roastmyopsec.com/blog/clear-site-data-vs-cookie-expiry-on-logout
- Category: Safety
- Updated: 2022-07-07

Clear-Site-Data vs cookie expiry on logout is two brooms. Setting the session cookie to Max-Age=0 (or an expired date) tells the browser to drop that cookie. Clear-Site-Data on the logout response can instruct supporting browsers to clear cookies, storage, and cache for the origin — including localStorage tokens a SPA left behind. You usually want both: expire the HttpOnly session cookie in the app, and send Clear-Site-Data if you also stored client-side state. RoastMyOpsec grades Set-Cookie on the landing page you paste. It does not log in or hit /logout, so a clean roast is not a logout-hygiene certificate.

### The practical difference

Cookie expiry is precise: one name, one host, one path. Miss a Domain or Path mismatch and the 'deleted' cookie is still there. Clear-Site-Data is blunt: cookies, cache, storage, executionContexts depending on the quoted values. It can surprise a marketing site that shares the origin with an app — you may wipe more than the session.

SPAs that put JWTs in localStorage cannot be saved by Max-Age=0 on a cookie they never used. See localStorage vs HttpOnly.

### How to choose based on the product

Cookie-session brochure plus WordPress: expire wordpress_ and session cookies on logout; Clear-Site-Data is optional. SPA on the same origin as the marketing site: logout must clear storage, not only the cookie; consider Clear-Site-Data "storage" plus explicit cookie expiry. Shared origin with a shop: do not blindly clear cache for the whole e-commerce CDN hostname.

| Control | What it clears | What it misses | Takeaway |
| --- | --- | --- | --- |
| Set-Cookie Max-Age=0 | That cookie name/path/domain | localStorage, other cookie names, Service Worker | Required for cookie sessions |
| Clear-Site-Data cookies | Origin cookies (supporting browsers) | HttpOnly cookies on another host; some browsers' gaps | Pair with explicit expiry |
| Clear-Site-Data storage | localStorage / IndexedDB class data | Tokens in memory until reload | Needed if you ever stored JS-visible sessions |
| Server session revoke | The server's idea of login | A stolen cookie still sent until expiry | Do this too — headers are not enough |

### What a public roast will not see

Logout is an authenticated POST. The free cookie check looks at Set-Cookie on the URL you submitted. Confirm logout in the app: 303 to a public page, expired session cookie, optional Clear-Site-Data, and a server-side session kill.

### Common mistakes

The first mistake is client-side-only 'logout' that deletes localStorage and leaves the HttpOnly cookie.

The second mistake is Clear-Site-Data on the homepage for everyone.

The third mistake is expiring the cookie with the wrong Path so the session cookie survives.

### FAQ

**Is Clear-Site-Data better than expiring the cookie?**

They stack. Always expire the session cookie and revoke the server session. Add Clear-Site-Data when client storage is in play.

**Will RoastMyOpsec test my logout?**

No. It does not log in. Check Set-Cookie on the public landing response, then test logout yourself.

**Does Clear-Site-Data log users out of other subdomains?**

It is origin-scoped in supporting browsers, not a whole eTLD+1 wipe. Cookie Domain=.example.com is a separate mistake — see cookie flags and prefixes.

---

## Certificate Expiry vs HSTS for Public Website TLS

- URL: https://roastmyopsec.com/blog/certificate-expiry-vs-hsts-for-public-sites
- Category: Safety
- Updated: 2022-06-20

Certificate expiry vs HSTS is not a pick-one control. A trusted, unexpired certificate is what makes this HTTPS visit work. HTTP Strict-Transport-Security is what tells browsers to skip HTTP next time. An expired or untrusted cert strands users even if HSTS is perfect. HSTS without a working cert turns a miss into a hard fail for every returning browser. Marketing sites need automated renewal, a long-enough HSTS max-age only after HTTPS is stable, and a redirect for first-time clients. RoastMyOpsec's free TLS check confirms the audited URL lands on HTTPS and that the runtime trust store accepts the cert. It is not a cipher-suite audit and it does not watch expiry for you.

### The practical difference

The certificate is a present-tense proof. Browsers and the RoastMyOpsec runtime either trust it now or they do not. Expiry, wrong hostname, and incomplete chains fail the handshake. Let's Encrypt and commercial CAs both expire; automation is the control, not the brand of the issuer.

HSTS is a future-tense memory. After a successful HTTPS response with Strict-Transport-Security, supporting browsers refuse cleartext to that host for max-age. includeSubDomains and preload raise the cost of a later rollback. They do not keep the cert alive.

### How to choose based on how ready TLS is

If renewal is still manual or a staging host shares the production name, keep HSTS max-age short or off until issuance is boring. If ACME, the CDN, and www plus apex all renew on schedule, raise max-age and keep the HTTP-to-HTTPS redirect for clients that have never seen the header.

Do not preload HSTS while any included subdomain still has a brittle cert. Preload plus an expired leaf is how you lock customers out of the brand until the CA and the preload list both forgive you.

| Control | What it proves | What it does not prove | Takeaway |
| --- | --- | --- | --- |
| Valid certificate now | This handshake is trusted by the client | Next month's renewal; HSTS memory | Required every minute the site is live |
| HSTS max-age | Later visits skip HTTP for browsers that cached it | That the next cert will issue on time | Turn on only after renewal is automated |
| HSTS preload | First visit HTTPS for browsers on the list | Operational recovery if TLS breaks | Last step, after every subdomain is ready |
| HTTPS redirect | This request's cleartext URL | Certificate health; mixed content | Keep it even after HSTS |

### When a valid cert still fails OPSEC

A green padlock on the homepage does not fix mixed content, HTTP campaign links, or a sibling subdomain that still serves HTTP. It also does not mean you monitor expiry. Teams that 'checked TLS once at launch' are the ones who discover Not After in the incident channel.

The free RoastMyOpsec TLS finding is scheme and trust at scan time. Pair it with your registrar, CDN, and ACME dashboards. The scan will not page you at 2 a.m. when the leaf dies.

### Common mistakes

The first mistake is turning on a two-year HSTS max-age before certificate renewal is automatic.

The second mistake is monitoring only the apex while www, the API host, and the CDN custom hostname expire on different calendars.

The third mistake is treating RoastMyOpsec's TLS-ok result as a substitute for expiry alerting.

### FAQ

**Is a valid TLS certificate enough without HSTS?**

It is enough for this visit if the client already used HTTPS. Without HSTS (and a redirect), first hops and old http:// links still speak cleartext. You want the cert, the redirect, and HSTS once HTTPS is stable.

**Does RoastMyOpsec check when my certificate expires?**

No. The free TLS check confirms HTTPS and that the runtime trust store accepts the certificate. Watch Not After in your CA, CDN, or ACME tooling.

**Should I disable HSTS if a cert might lapse?**

Fix renewal first. Turning HSTS off does not make expiry safe; it only restores a cleartext first hop. If you must roll back HTTPS, drop max-age carefully and understand preload removal is slow.

---

## Cache-Control public vs private for Personalized Pages

- URL: https://roastmyopsec.com/blog/cache-control-public-vs-private-for-personalized-pages
- Category: Safety
- Updated: 2022-05-17

Cache-Control public vs private is a shared-cache decision, not a performance slogan. public means a CDN or browser may store the response for reuse across users. private means only the end user's browser should keep it. Authenticated or personalized HTML that also sets cookies should be private or no-store — not public. Static assets can stay public with long max-age. RoastMyOpsec inspects Cache-Control versus Set-Cookie on the primary response. It does not poison caches or fetch other users' pages.

### The practical difference

A shared cache that stores a page with someone else's session cookie header — or a body that includes their name — will serve that mix to the next visitor. That is the failure mode, not 'the CDN is too fast.'

private still allows the browser to cache. no-store is for responses you do not want replayed from disk at all, such as account dashboards after logout is a concern.

### How to choose based on the response

Separate route classes. Marketing HTML can often be public if it is the same for everyone and does not Set-Cookie a session. App HTML that varies by account should be private or no-store. JS/CSS/images with content hashes can be public and immutable.

| Response | Cache-Control | Cookies | Takeaway |
| --- | --- | --- | --- |
| Public marketing HTML | public, short max-age or s-maxage | None, or non-session prefs only | Fine when the body is identical for all |
| Logged-in HTML | private, no-store, or no-cache | Session cookies likely | Never public on a shared cache |
| Hashed static assets | public, max-age long, immutable | None | This is where CDNs earn rent |
| API JSON with PII | private or no-store | If used, still not public | Do not let intermediaries reuse bodies |

### When public still fails the roast

public on a document that also sends Set-Cookie is the classic miss. Even if the body looks generic, caches can store the cookie semantics poorly and browsers can get confused about what is shareable.

Fix at the origin and at the CDN: explicit rules per path prefix, not one global 'cache everything' toggle.

### Common mistakes

The first mistake is copying a static-site Cache-Control onto an app shell.

The second mistake is Vary: Cookie without understanding you just exploded cache keys — or omitted Vary and shared the wrong body.

The third mistake is purging the CDN once and leaving the origin header wrong.

### FAQ

**Is Cache-Control public a vulnerability?**

Not by itself. It is a miss when the response is personalized or paired with session cookies. Public is appropriate for identical, unauthenticated assets.

**Should I use private or no-store for dashboards?**

private allows the user's browser to reuse the page. no-store is stricter when you do not want that file on disk. Many account pages use private, no-cache plus a solid logout.

**How does RoastMyOpsec grade caching?**

Header inspection on the primary response: Cache-Control versus Set-Cookie combinations. No cache-poisoning tests.

---

## CAA Records vs Registrar Lock for Domain OPSEC

- URL: https://roastmyopsec.com/blog/caa-records-vs-registrar-lock
- Category: Safety
- Updated: 2022-05-09

CAA records vs registrar lock are two different doors on the same house. Certification Authority Authorization (CAA) is a DNS record that tells public CAs which issuers are allowed to mint certificates for your domain. Registrar lock (clientTransferProhibited) blocks a class of domain-theft transfers. CAA does not stop someone who already controls your DNS from changing CAA. Lock does not stop a CA from issuing if your DNS still allows them. Marketing sites should publish a tight CAA for the CA they actually use, keep transfer lock on, and still run MFA on the registrar. RoastMyOpsec inventories public NS for hygiene context. It does not attempt transfers, issuance, or CAA forging.

### The practical difference

CAA is issuance policy. A CA that checks CAA should refuse to issue if your record does not name them (or if you used issuewild vs issue incorrectly for wildcards). It is a speed bump against surprise certificates, not a cryptographic proof that no cert exists.

Registrar lock is account policy. It makes unauthorized transfers fail until you unlock on purpose. Combined with unique passwords and MFA, it is how you keep the zone from walking out the registrar's front door.

### How to choose what to ship this week

If the registrar account is still on a shared mailbox password, lock and MFA first. Then add CAA for the CA your CDN or ACME client actually uses (including issuewild if you mint *.example.com). If you use two issuers during a migration, list both until cutover, then drop the old one.

Do not copy someone else's CAA that names a CA you never configured. Failed issuance is an outage with extra steps.

| Control | What it limits | What it does not limit | Takeaway |
| --- | --- | --- | --- |
| CAA issue / issuewild | Which public CAs should issue for the name | Attackers who already edit your DNS; private PKI | Publish for the CA you use; keep it current |
| Registrar transfer lock | Surprise domain transfers | Compromised registrar login that can unlock first | On by default; MFA the account |
| DNSSEC | Forged DNS answers when validators check | Unlocked registrar; stale CAA | Pair with hygiene; see the DNSSEC guide |
| NS inventory | Knowing who answers the zone | Issuance policy | Start here so CAA is on the live nameservers |

### When CAA still fails OPSEC

CAA on the apex only, while www or the API host lives in a different zone, is a common miss. Wildcards need issuewild. A leftover CAA that names last year's CA is how ACME breaks on deploy day.

A URL roast will not prove your CAA is correct. Confirm in DNS and in your CA's issuance logs. The scan is not a certificate-transparency hunt.

### Common mistakes

The first mistake is CAA without lock, as if issuance policy stops domain theft.

The second mistake is lock without CAA, then wondering why a surprise cert appeared in a dashboard.

The third mistake is setting CAA on a staging zone that is not the zone browsers actually query.

### FAQ

**Is CAA better than registrar lock?**

They solve different problems. Lock the registrar. Publish CAA for your real CA. Do both.

**Does RoastMyOpsec check my CAA records?**

The public DNS work is nameserver inventory for hygiene, not a full CAA or DNSSEC validator. Confirm CAA at your DNS host.

**Will CAA stop mixed content or expired certs?**

No. CAA is about who may issue. Expiry, HSTS, and mixed content are separate guides.


# Vulnerabilities

## yarn.lock vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/yarn-lock-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2026-08-31

yarn.lock vs package.json on a production origin is two Node inventory files that must not sit next to index.html. /package.json donates names and versions — the roast signatures a JSON body with a name field. yarn.lock donates the exact pins, richer for advisory homework. Neither is a pentest, both are maps. RoastMyOpsec may not fetch yarn.lock today. Still deny it at the edge. Do not Disallow lockfiles in robots.txt; deny the objects, do not advertise them.

### The practical difference

package.json is the brochure of declared dependencies. yarn.lock is the receipt. Attackers use either as a shopping list. Defenders use lockfiles in private CI and Dependabot — not as CDN files.

Removing /package.json does not patch CVEs. Publishing yarn.lock does not replace a private advisory scan. Fix the public file the same day; schedule the upgrade work separately.

### How to choose based on what you shipped

Fail CI if package.json or yarn.lock appear in a static marketing artifact. Deny those basenames at the CDN. Deepen a repo you own for lockfile advisories — that is a different channel than a URL roast. See public package.json vs lockfile advisories.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /package.json | Almost never on the CDN | Declared names and scripts | Roast signatures this path today |
| /yarn.lock | Never on the CDN | Exact pins, richer map | Same class of miss; deny the name |
| Lockfile in private git / CI | Reproducible installs and alerts | Do not export it with the site | Dependabot belongs on the repo |
| robots.txt Disallow | Not for lockfiles | Publishes the path | Edge deny instead |

### When each inventory file wins (off the public origin)

package.json wins in the private repo and on the npm registry, not on your brochure host. yarn.lock wins in that same private repo so installs and alerts stay honest. Neither wins as a public object beside index.html.

If Dockerfile shipped in the same export, fix the publish root once. See Dockerfile vs package.json.

### Common mistakes

The first mistake is blocking package.json and still serving yarn.lock.

The second mistake is treating a public lockfile as proof you already ran a complete advisory review.

The third mistake is listing yarn.lock in robots.txt Disallow instead of denying it at the CDN.

### What a URL roast can prove

Bounded GET to /package.json with a name-field signature. yarn.lock is not a scored path today. Still deny it. No npm install from your site, no exploit of a listed package. A missing package.json is not a claim your lockfile is clean.

### FAQ

**Is a public yarn.lock a vulnerability?**

It is an information leak. Block it. It is not remote code execution, and it is not harmless inventory on a marketing origin.

**Does RoastMyOpsec fetch yarn.lock?**

Not as a scored path today. It does probe /package.json. Deny yarn.lock at the CDN anyway. Same class of miss.

**Should I keep lockfiles public so users can audit me?**

Publish a security page or a public repo if you want that. The CDN next to index.html is the wrong channel.

---

## wp-config.php.bak vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/wp-config-bak-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2026-07-19

wp-config.php.bak vs .env on production origins is two costumes of live secrets in the document root. /.env holds framework secrets. wp-config.php is supposed to be executed as PHP, not served as text — but editors leave wp-config.php.bak, .save, .old, and wp-config.php~ that many static hosts will GET as plaintext: DB_NAME, DB_PASSWORD, AUTH_KEY. Neither belongs next to index.html. RoastMyOpsec probes /.env and /.env.local with bounded GETs looking for signatures. It does not currently score wp-config.php.bak, does not parse PHP configs, and does not use leaked keys.

### The practical difference

A correctly served wp-config.php runs and returns nothing useful to a GET. A backup extension often bypasses PHP and dumps the file. .env is always a static object if the origin serves it. debug.log is a cousin leak. composer.json is inventory, not usually the password — see those guides.

robots.txt Disallow: /wp-config.php.bak advertises the name. Deny at the CDN instead.

### How to choose a deny list

CI: fail if .env, wp-config.php.bak, wp-config.php.save, wp-config.php.old, or *~ editor files appear in the artifact. CDN: deny those suffixes even if CI is perfect. Never copy wp-config into a public bucket. Staging hostnames that are guessable get the same rules. Rotate DB passwords if a backup was ever world-readable.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |
| /wp-config.php.bak | DB and salts as text | Not scored; deny anyway | Same class |
| /wp-config.php (executed) | Should be empty to GET | Not a scored path | Keep PHP execution on |
| /wp-content/debug.log | Error inventory | Not scored | See debug.log vs framework debug |

### When wp-config still wins (off the public origin)

The live PHP file on a server that executes it is how WordPress boots. A downloadable backup never wins. Pair with .htaccess vs web.config if the same zip also shipped IIS/Apache config.

### What the roast can prove

Signature hits on /.env can surface. wp-config backups are not scored paths. Confirm a deny at the edge. Pair with WordPress XML-RPC if the same host also left an extra auth surface.

### Common mistakes

The first mistake is editing wp-config on production over FTP and leaving .bak in place.

The second mistake is allowing .bak because 'PHP would run it' on a host that serves unknown extensions as text/plain.

The third mistake is Disallow: /wp-config.php.bak and calling it access control.

### FAQ

**Is a public wp-config.php.bak a vulnerability?**

Yes — treat it as a credential leak. Deny the name, delete the file, and rotate secrets if it was reachable.

**Does RoastMyOpsec fetch wp-config backups?**

Not as scored paths today. It does probe .env. Deny backup suffixes at the edge anyway.

**Will the scanner use leaked database passwords?**

No. Bounded GETs and signatures only. No recursive dump, no exploit payload.

---

## WordPress XML-RPC vs wp-login.php for Public Site OPSEC

- URL: https://roastmyopsec.com/blog/wordpress-xmlrpc-vs-wp-login
- Category: Vulnerabilities
- Updated: 2026-07-10

WordPress XML-RPC vs wp-login.php is a two-door problem on a lot of marketing CMSs. wp-login.php is the browser form. xmlrpc.php is a legacy remote-procedure endpoint that can still authenticate users if it is enabled. Hardening only the form leaves the API door lonely. If you do not use Jetpack, the mobile app, or a pingback you actually want, disable XML-RPC. If you must keep it, put the same WAF rate limits, bot fights, and MFA on that path as on wp-login.php. RoastMyOpsec may notice common admin/login paths with soft GETs. It does not spray passwords, does not send XML-RPC auth methods, and does not prove your limits work.

### The practical difference

The login page is a document with cookies and, if you did it right, MFA. XML-RPC is machine talk. Features that needed it in 2012 are often unused on a 2026 brochure site. Presence of /xmlrpc.php is discoverability, not a breach. A 200 that still accepts authentication is a second stuffing target.

Pingbacks and trackbacks are a separate product decision. Most marketing sites can live without them.

### How to choose based on the site

Pure brochure WordPress: disable XML-RPC at the host or with a maintained plugin, rate-limit wp-login.php, MFA for administrators, keep the site updated. Editorial shops that need the official mobile app: keep XML-RPC or the replacement API, but do not leave it unmetered. Do not 'hide' wp-login.php as the only control.

| Door | Who uses it | When it wins to keep | Takeaway |
| --- | --- | --- | --- |
| wp-login.php | Humans in a browser | You edit the site | MFA + rate limit + CAPTCHA/WAF |
| xmlrpc.php | Apps, Jetpack, old pingbacks | A real feature still depends on it | Same abuse controls; else disable |
| /wp-admin/ | After login | Always, for editors | Discoverability ≠ bypass; see admin guide |
| REST /wp-json/ | Blocks, headless, apps | The theme needs it | Authz on routes; not the same as XML-RPC |

### What a URL roast will not do

No XML method lists as an attack, no credential stuffing, no pingback amplification tests. A missing xmlrpc.php or a deny at the edge is a healthy signal. A 200 on the path is an inventory item — confirm in WordPress whether the feature is on, then decide.

### Common mistakes

The first mistake is CAPTCHA on wp-login.php and unlimited XML-RPC.

The second mistake is renaming wp-login.php and leaving xmlrpc.php stock.

The third mistake is robots.txt Disallow for xmlrpc as if that authenticated anyone.

### FAQ

**Is xmlrpc.php a vulnerability by itself?**

No. It is an extra authentication surface. Unused, it should be off. Used, it needs the same abuse controls as login.

**Will RoastMyOpsec brute-force WordPress?**

No. Soft GETs for presence and status. No password spraying, no XML-RPC auth attempts.

**Should I hide wp-login.php instead of disabling XML-RPC?**

Hiding is not authentication. Disable unused XML-RPC, MFA the real login, and rate-limit both doors you keep.

---

## WordPress debug.log vs Framework Debug on Production

- URL: https://roastmyopsec.com/blog/wordpress-debug-log-vs-framework-debug
- Category: Vulnerabilities
- Updated: 2026-07-02

WordPress debug.log vs framework debug on production is two leftover operator surfaces. WP_DEBUG plus WP_DEBUG_LOG writes PHP notices and often paths into wp-content/debug.log — a file that too often is fetchable as a static object. APP_DEBUG / Django DEBUG=True turns every 500 into a yellow page. phpinfo() is a cousin dump. Production belongs on generic client errors and logs that are not in the document root. RoastMyOpsec does not currently score /wp-content/debug.log. It may heuristic-scan common API error bodies. It does not set WP_DEBUG, does not download logfiles, and does not send exploit payloads.

### The practical difference

debug.log is a file. Framework debug is a response. XML-RPC and wp-admin are other WordPress surfaces — see those guides. display_errors in php.ini is yet another switch. Turning off WP_DEBUG_DISPLAY while leaving the log in a public directory is a common stall.

robots.txt Disallow: /wp-content/debug.log advertises the name. Deny at the CDN instead.

### How to choose production settings

WP_DEBUG false, WP_DEBUG_LOG false, WP_DEBUG_DISPLAY false on production. Logs: server syslog or a private path outside the web root. CDN: deny debug.log, debug.log.*, and error_log names. CI: fail if WP_DEBUG is true in the production wp-config. Staging: debug behind auth, not on the marketing host.

| Surface | Typical leak | Public origin | Takeaway |
| --- | --- | --- | --- |
| /wp-content/debug.log | Paths, plugin errors, sometimes queries | No | Not a scored roast path; deny anyway |
| WP_DEBUG true (on-page) | Notices in HTML | No | See framework debug |
| /phpinfo.php | Runtime dump | No | See phpinfo vs verbose errors |
| XML-RPC | Auth surface | Disable if unused | See XML-RPC vs wp-login |

### When a debug log still wins (off the public origin)

Logging to a private file or SIEM still wins for operators. The miss is a world-readable object on 443. Pair with composer.json if the same deploy also published PHP inventory.

### What the roast can prove

Heuristic error-body hits can surface. debug.log is not a scored path. A 404 is not proof WP_DEBUG_LOG is off. Confirm wp-config on the host you submitted. Pair with exposed .env if secrets leaked from a public file.

### Common mistakes

The first mistake is WP_DEBUG_LOG true 'only for this hotfix' on the brand hostname.

The second mistake is moving the log to /wp-content/uploads because it was easier.

The third mistake is Disallow: /debug.log and calling it access control.

### FAQ

**Is a public debug.log a vulnerability?**

Yes — it is an information-leak class. Delete it, deny the name, and turn WP_DEBUG_LOG off.

**Does RoastMyOpsec fetch debug.log?**

Not as a scored path today. Deny /wp-content/debug.log at the edge anyway.

**Does Cloudflare hide the log?**

No. It proxies whatever your origin returns unless you add a deny rule.

---

## WebSockets vs CORS for Browser APIs

- URL: https://roastmyopsec.com/blog/websocket-vs-cors-for-browser-apis
- Category: Vulnerabilities
- Updated: 2026-06-15

WebSockets vs CORS for browser APIs is two different browser gates. CORS decides whether fetch/XHR may read a cross-origin HTTP response. A WebSocket starts as HTTP Upgrade; after the handshake, frames are not CORS-checked. Access-Control-Allow-Origin on the REST API does not protect wss://. The server must verify Origin (and cookies or tokens) on the upgrade. Reflecting any Origin or skipping the check is the miss. RoastMyOpsec app mode sends a bounded untrusted Origin on HTTP and reads ACAO. It does not open WebSockets, does not send upgrade frames, and does not score missing Origin checks on wss.

### The practical difference

fetch() to https://api.example/data is CORS. new WebSocket('wss://api.example/ws') is a handshake the page's origin is sent on. Same-site cookies may attach depending on SameSite. JSONP is a third gate — script tags — already retired in the JSONP vs CORS guide.

SameSite=None cookies on a WebSocket cookie-auth API are an embed decision. Prefer tokens in the handshake your app already uses for HTTP, not a second cookie policy.

### How to choose a gate

Public read-only WS: still allowlist Origin so random sites cannot open a socket on your users' behalf if the socket is cookie-authenticated. Anonymous market-data sockets can allow more origins but should not trust the client as a user. Authenticated sockets: Origin allowlist plus a session or ticket. Do not copy CORS * onto the upgrade.

| Channel | Browser gate | Server must | Takeaway |
| --- | --- | --- | --- |
| fetch / XHR | CORS | ACAO allowlist; still auth the user | See wildcard vs allowlist |
| WebSocket | No CORS on frames | Check Origin; authenticate upgrade | CORS headers do not apply |
| JSONP | Script tag | Retire it | See JSONP vs CORS |
| Same-origin WS | Same origin as the page | Still authenticate | Origin check is easy to get right |

### What the roast can prove

HTTP ACAO and credentials behavior can surface. A 101 is not in the scan. Confirm Origin handling on your own upgrade path. Pair with CORS credentials vs wildcard if the socket uses cookies.

### Common mistakes

The first mistake is tightening CORS and leaving the WebSocket Origin check as *.

The second mistake is cookie-auth sockets with SameSite=None for a widget you do not trust.

The third mistake is treating a roast ACAO pass as proof the socket is locked.

### FAQ

**Does CORS protect WebSockets?**

No. Frames after upgrade are not CORS-gated. Check Origin and authenticate on the handshake.

**Should every site Origin-check WebSockets?**

If the socket is authenticated or user-specific, yes. Anonymous public streams still benefit from an allowlist.

**Does RoastMyOpsec open WebSockets?**

No. It does not send Upgrade or score wss Origin handling.

---

## .vscode vs .DS_Store on Production Origins

- URL: https://roastmyopsec.com/blog/vscode-folder-vs-ds-store-on-production
- Category: Vulnerabilities
- Updated: 2026-05-03

.vscode vs .DS_Store on production origins is two leftover folders next to index.html. .DS_Store is Apple Finder metadata that can list filenames. .vscode (and JetBrains .idea) is editor config: launch.json, tasks.json, settings.json, sometimes extensions.json. Those files can name local paths, debug ports, and — when someone pasted a secret into a workspace setting — tokens. Neither is access control. Autoindex off does not delete them. RoastMyOpsec probes /.DS_Store with a bounded GET and a binary signature. It does not currently score /.vscode or /.idea, does not parse JSON settings, and does not crawl every prefix looking for nested leftovers.

### The practical difference

Finder files are OS cache. IDE folders are project config that hitchhiked into the static zip. .env.example is a cousin template. package.json is inventory if you published it. Git is a third channel if /.git is public.

A 200 on /.vscode/settings.json is an information leak even when the values look boring. Deny the directory at the CDN.

### How to choose what to strip

CI: fail the build if .vscode, .idea, .DS_Store, or desktop.ini appear in the CDN artifact. Keep editor folders in the private repo only. CDN: deny those prefixes even if CI is perfect. Nested /assets/.vscode counts. Do not 'clean once' on a laptop and call it a pipeline.

| Leftover | Typical leak | Public origin | Takeaway |
| --- | --- | --- | --- |
| /.DS_Store | Filenames from Finder | Never | Scored path on the roast |
| /.vscode/* | Paths, tasks, sometimes secrets | Never | Not scored today; deny anyway |
| /.idea/* | JetBrains workspace | Never | Same class |
| /.env.example | Variable names / copied values | Never on the CDN | See .env.example vs .env |

### When .vscode still wins (off the public origin)

Shared recommended extensions in a private repo help onboarding. They never belong on a marketing bucket. Pair with exposed .git if the same zip also shipped history.

### What the roast can prove

A .DS_Store signature on a bounded GET can surface. .vscode is not scored. Confirm a 404 yourself. Pair with directory listing if the folder is also autoindexed.

### Common mistakes

The first mistake is stripping only .DS_Store because Mac laptops were the known problem.

The second mistake is committing settings.json with a live API key 'just for this workspace.'

The third mistake is Disallow: /.vscode and calling it access control.

### FAQ

**Is a public .vscode folder a vulnerability?**

It is an information leak. Remove it and deny the prefix. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch .vscode?**

Not as a scored path today. It does signature-check .DS_Store. Deny editor folders at the edge anyway.

**Should recommended extensions stay in the repo?**

Yes, in the private git repo. Not as objects on the public CDN next to index.html.

---

## Verbose API Errors vs Generic Client Messages

- URL: https://roastmyopsec.com/blog/verbose-api-errors-vs-generic-client-messages
- Category: Vulnerabilities
- Updated: 2026-04-25

Verbose API errors vs generic client messages is a production-logging split. Clients should get a short, stable code and a human sentence that does not name files, SQL, or framework versions. Operators should get the stack in logs you control. Chatty 500 bodies are reconnaissance. RoastMyOpsec's app-mode check does a soft GET to common API paths and looks for stack-trace or path-leak heuristics in small body snippets. It does not fuzz parameters or dump databases.

### The practical difference

A generic message is 'Something went wrong' plus a request id. A verbose message is a Spring/Django/Node stack, a SQLSTATE, or /var/www/app/src/db.ts:214. The second one teaches strangers how to aim.

Debug mode in a framework is the usual source. Staging can be loud. Production should not copy that flag.

### How to choose what the client sees

Map exception classes to public codes (validation, auth, not found, conflict, unavailable). Log the rest with the request id. Never send the exception message string to the browser if it was built from a driver error.

| Audience | Contents | Where | Takeaway |
| --- | --- | --- | --- |
| End user / SPA | Stable code, short copy, request id | JSON body | Boring is correct |
| On-call engineer | Stack, SQL, headers (redacted) | Server logs / APM | This is where the novel belongs |
| Attacker reading /api | Whatever you accidentally shipped | Same JSON they fetched | Do not donate paths and versions |
| Support ticket | Request id only | User paste | Lets you find the log line |

### When a 200 with an error object is still a miss

Some APIs return HTTP 200 with { error: <driver text> }. Status codes are not the only leak. GraphQL errors arrays can name fields and internal types even when introspection is off.

Pair generic bodies with the GraphQL and CORS guides: one chatty surface is enough to map the rest.

### Common mistakes

The first mistake is NODE_ENV=development in production 'just for this hotfix.'

The second mistake is returning err.message from a catch-all.

The third mistake is detailed errors on /api and generic ones on /.

### FAQ

**Are stack traces in API JSON a CVE?**

Usually information disclosure, not remote code execution. They still speed up every other bug. Remove them from production responses.

**Can I keep detailed errors behind an admin header?**

A debug header that anyone can send is not a control. Prefer environment config and authenticated operator tools.

**How does RoastMyOpsec detect chatty errors?**

A bounded GET to common API paths and a heuristic on small body snippets. No fuzzing, no exploit payloads.

---

## Timing-Allow-Origin vs CORS for Cross-Origin Telemetry

- URL: https://roastmyopsec.com/blog/timing-allow-origin-vs-cors
- Category: Vulnerabilities
- Updated: 2026-02-24

Timing-Allow-Origin vs CORS is two different yeses. CORS (Access-Control-Allow-Origin and friends) is whether another origin's JavaScript may read the body and many headers. Timing-Allow-Origin (TAO) is whether the Resource Timing API exposes detailed timestamps for that request instead of a coarse bucket. A static font on a CDN may send TAO * so first-party RUM can see load times. An authenticated API that also sends TAO * (or CORS *) is donating extra cross-origin telemetry it may not need. RoastMyOpsec's CORS check is about allowlists vs wildcards on API-shaped responses. It does not grade TAO and it does not run timing measurements against you.

### The practical difference

CORS is an access-control conversation (including preflight OPTIONS). TAO is a stopwatch conversation. You can allow CORS reads without TAO, and you can unmask timing without allowing the body to be read. Copy-pasting both * from a performance blog is how APIs inherit a CDN static-asset policy.

Resource Timing details are not a substitute for a pentest and not a claim of a specific attack. They are extra data other pages can observe about how your asset loaded.

### How to choose based on the resource

Public JS/CSS/fonts you want RUM on: TAO for the origins that run your analytics, or * if the files are truly public and boring. JSON APIs with cookies or tokens: CORS allowlist only; leave TAO off unless you have a documented RUM need. Do not set ACAO * with credentials.

| Header | What another origin's JS gets | Typical static CDN | Typical private API |
| --- | --- | --- | --- |
| CORS allowlist | Readable body (if allowed) | Often unused | Named origins only |
| CORS * | Public read, no credentials | OK for truly public JSON | Usually wrong |
| TAO * | Unmasked Resource Timing | Common for RUM | Usually unnecessary |
| Neither | Opaque / coarse timing | Fine if you do not need RUM on that host | Healthy default |

### What the roast can see

Wildcard CORS on app-mode responses is a finding. TAO is not scored. Confirm both at the CDN so HTML, assets, and APIs do not share one header soup. See CORS wildcard vs allowlist.

### Common mistakes

The first mistake is TAO * on the same origin as Set-Cookie sessions.

The second mistake is copying a Cloudflare static-asset header pack onto /api.

The third mistake is treating TAO as a CORS substitute.

### FAQ

**Is Timing-Allow-Origin * a vulnerability?**

On public static files it is usually a RUM choice. On authenticated APIs it is extra telemetry you should justify. It is not the same as ACAO *.

**Does RoastMyOpsec check TAO?**

No. The CORS check is allowlist vs wildcard on API-shaped responses. Confirm TAO in your CDN if RUM depends on it.

**Do I need TAO for CORS to work?**

No. They are independent. Set CORS for data access and TAO only if you need detailed Resource Timing.

---

## Thumbs.db vs .DS_Store on Production CDNs

- URL: https://roastmyopsec.com/blog/thumbs-db-vs-ds-store-on-production
- Category: Vulnerabilities
- Updated: 2026-02-16

Thumbs.db vs .DS_Store on production CDNs is the same OPSEC class in two operating-system costumes. .DS_Store is Apple Finder folder metadata. Thumbs.db (and ehthumbs.db) is a Windows thumbnail cache that can list image names from a folder someone zipped. Neither belongs next to index.html. Autoindex off does not delete them. RoastMyOpsec probes /.DS_Store with a bounded GET and a binary signature. It does not currently treat Thumbs.db as a scored path, does not parse thumbnail databases, and does not crawl every prefix looking for nested leftovers.

### The practical difference

Mac teams ship .DS_Store. Windows teams ship Thumbs.db when Explorer cached a folder of screenshots or product photos. Mixed shops ship both. The leak is filenames and sometimes dimensions — not usually the file bytes, but enough to map backups.zip sitting beside them.

desktop.ini is a cousin. Add all three to the publish denylist.

### How to choose what to strip

CI: fail the build if Thumbs.db, .DS_Store, or desktop.ini appear in the artifact. CDN: deny those names even if CI is perfect. Do not 'clean once' on a laptop and call it a pipeline.

| Leftover | Typical OS | What it often reveals | Takeaway |
| --- | --- | --- | --- |
| /.DS_Store | macOS | Filenames from Finder | Scored path on the roast |
| /Thumbs.db | Windows | Image names from Explorer | Same class; strip anyway |
| desktop.ini | Windows | Folder view leftovers | Deny with the others |
| Index of / | Web server | Every object | See directory listing guide |

### What the roast can prove

A .DS_Store signature hit is a finding. A 404 on Thumbs.db is not a certificate that the file is absent under /assets/2024/. Edge deny is cheap. Pair with dump.sql vs backup.zip if archives hitchhiked in the same zip.

### Common mistakes

The first mistake is gitignoring .DS_Store and still uploading a Windows zip of the dist folder.

The second mistake is deleting Thumbs.db locally and not denying the name at the CDN.

The third mistake is treating a roast miss on Thumbs.db as 'Windows leftovers are fine.'

### FAQ

**Is Thumbs.db a vulnerability?**

It is an information leak. Remove it and deny the name. It is not a credential dump.

**Does RoastMyOpsec check Thumbs.db?**

The bounded path list includes .DS_Store. Treat Thumbs.db as the same hygiene even if this scan does not score it.

**Will the scanner parse thumbnail files?**

No. Signature checks only. No recursive dump, no exploit payload.

---

## Terraform tfstate vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/terraform-tfstate-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2026-01-30

Terraform tfstate vs .env on a production origin is two files that must never be public objects. /.env is app secrets — the roast signatures it. terraform.tfstate is a JSON snapshot of infrastructure that HashiCorp documents as sensitive: resource attributes can include passwords, tokens, and private endpoints. Serving it next to index.html is the same class of miss as /.env. RoastMyOpsec may not fetch tfstate today. Still deny that name at the edge. Do not list it in robots.txt.

### The practical difference

.env is what the website process reads. tfstate is what Terraform reads to remember the cloud. Different tools, same rule: the marketing bucket is not a state backend.

Remote state with encryption and lock is the intended home. A leftover terraform.tfstate in a static export is an accident that looks like a brochure file until someone downloads it.

### How to choose based on what you shipped

If tfstate was public: take it off the origin, deny the basename at the CDN, rotate secrets that may have been in state, and move state to a private backend. Fail CI if .tfstate appears in a frontend artifact. Pair with /.env if the same zip held both.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| .env on the origin | Never | App secrets | Incident; roast signatures this path |
| terraform.tfstate on the origin | Never | State can hold the same class of secrets | Same class of miss; deny the name |
| Remote state backend | Team Terraform you control | A public ACL on the backend recreates the miss | Not the website CDN |
| robots.txt Disallow | Not for state files | Advertises the path | Edge deny instead |

### When each file wins (never as a static download)

.env wins as private app runtime config. tfstate wins in a locked, private backend. Local tfstate wins only on a workstation that never syncs that folder to a public bucket.

Marking variables sensitive redacts CLI output. It does not make a public tfstate safe. HashiCorp's own guidance treats state as sensitive data.

### Common mistakes

The first mistake is keeping terraform.tfstate in the same repo as the marketing site and deploying the repo root.

The second mistake is rotating .env values and leaving state on the CDN because 'that is infrastructure, not the app.'

The third mistake is Disallowing *.tfstate in robots.txt instead of blocking the objects at the CDN.

### What a URL roast can prove

The roast signatures /.env. terraform.tfstate is not a scored path today. Still deny it. Bounded GETs only. No state parse as an exploit recipe, no cloud API use. Confirm the bucket. A homepage body on that path is a soft-404, not a pass for the file.

### FAQ

**Is a public terraform.tfstate a vulnerability?**

Treat it as a sensitive-data exposure. Remove it, deny the name at the edge, and rotate secrets that may have been in state.

**Does RoastMyOpsec fetch terraform.tfstate?**

Not as a scored path today. It does probe /.env. Deny tfstate at the CDN anyway. Same class of miss as /.env.

**Does sensitive = true make public state safe?**

No. That flag redacts typical CLI output. State can still hold the values. Keep state off public origins.

---

## Swagger UI vs GraphQL Introspection on Production APIs

- URL: https://roastmyopsec.com/blog/swagger-ui-vs-graphql-introspection
- Category: Vulnerabilities
- Updated: 2026-01-21

Swagger UI vs GraphQL introspection on production APIs is two ways to publish the schema. Swagger UI /swagger, /docs, and /openapi.json are interactive catalogs. GraphQL introspection (__schema) is a query that dumps types. Neither is a credential, both shrink reconnaissance and sometimes show unreleased fields. Keep docs on a private host. Disable introspection in production. RoastMyOpsec app mode may send one bounded unauthenticated GraphQL introspection POST to /graphql. It does not currently score Swagger UI or openapi.json, does not click 'Try it out,' and does not send mutation payloads.

### The practical difference

OpenAPI files are documents. GraphQL introspection is a live query. Authentication on the UI is not enough if openapi.json is still a public GET. robots.txt Disallow: /swagger advertises the path.

Verbose API errors and framework debug are cousins: they donate internals without a full schema browser. See those guides too.

### How to choose what to publish

Public product APIs: a curated docs site, not a raw Try-it-out UI against production. Partner APIs: auth on both the UI and the spec URL. Internal: VPN. GraphQL: introspection off in production; use a build-time schema for your own clients.

| Surface | Typical leak | First response | Takeaway |
| --- | --- | --- | --- |
| /swagger / /docs | Interactive schema + try-it | Remove from public origin | Not a scored roast path today |
| /openapi.json | Machine-readable map | Auth or private host | Same class as the UI |
| GraphQL __schema | Live type graph | Disable in production | Roast may probe /graphql |
| DEBUG / APP_DEBUG | Stack on every 500 | Turn off | See framework debug |

### What the roast can prove

A GraphQL introspection hit can surface in app mode. A 200 on /swagger is not scored today — still remove it. Pair with CORS wildcard if the docs origin can call the API with cookies.

### Common mistakes

The first mistake is Swagger UI on api.example.com because 'devs need it.'

The second mistake is auth on /swagger and none on /v3/api-docs.

The third mistake is disabling introspection and leaving GraphiQL at /graphiql.

### FAQ

**Is public OpenAPI a vulnerability?**

It is an information map. For a public API you already document, curated docs are fine. An unauthenticated Try-it-out UI against production is the miss.

**Does RoastMyOpsec fetch Swagger UI?**

Not as a scored path. GraphQL introspection on /graphql may be probed in app mode — read-only, no mutations.

**Is GraphiQL the same as Swagger?**

Same class: an interactive schema browser. Keep it off production.

---

## Subresource Integrity vs Unpinned CDN Scripts

- URL: https://roastmyopsec.com/blog/subresource-integrity-vs-unpinned-cdn-scripts
- Category: Vulnerabilities
- Updated: 2026-01-13

Subresource Integrity vs unpinned CDN scripts is a supply-chain control, not a speed debate. SRI (the integrity attribute) tells the browser to refuse a script or stylesheet whose hash does not match. Unpinned CDN URLs — especially versionless or jQuery 1.x/2.x leftovers — execute whatever that URL returns today. Self-hosting critical JS is often simpler than hashing a moving vendor file. RoastMyOpsec parses public HTML for cross-origin script tags, missing integrity, and explicit legacy jQuery patterns. It does not download exploit kits or mutate your CDN.

### The practical difference

A CDN URL without integrity is a live remote. If the host is compromised, the name is reused, or a 'latest' path moves, your visitors run new code under your origin's pages.

SRI pins a hash. The file can still be served from a CDN for performance, but the browser will not execute a surprise body. If you cannot keep the hash in lockstep with releases, self-host the exact file you tested.

### How to choose based on the asset

First-party bundles belong on your origin. Third-party widgets should be the smallest set you can justify, with integrity when the vendor publishes hashes, plus a CSP that names those hosts. Analytics tags that rewrite themselves weekly may not be SRI-friendly — that is a reason to cut them, not to skip the question.

| Pattern | SRI? | Better default | Takeaway |
| --- | --- | --- | --- |
| Self-hosted app bundle | Optional | Your origin, hashed filename | You already pin via the build |
| cdn.jsdelivr / unpkg without integrity | Required if you keep the CDN | Self-host or add integrity | Classic unpinned miss |
| Legacy jQuery 1.x / 2.x from a public CDN | Does not fix old bugs | Upgrade or remove | Hashing museum code still leaves known holes |
| Tag manager | Usually no stable hash | Minimize; CSP-shape the rest | SRI cannot save an unbounded loader |

### When SRI is not enough

Integrity does not review what the original file does. A hashed tracker still runs tracker code. Pair SRI with CSP script-src and a shorter third-party list.

crossorigin is required on many cross-origin SRI tags so the browser can check the hash. Missing it makes the integrity attribute fail closed or be ignored depending on the browser — confirm in a site you own.

### Common mistakes

The first mistake is copying a CDN snippet from 2016 and never pinning it.

The second mistake is hashing a file, then letting the src query string float to a new version.

The third mistake is putting SRI on CSS while leaving the real XSS surface — scripts — unpinned.

### FAQ

**Does SRI replace Content-Security-Policy?**

No. SRI pins a file's bytes. CSP limits which origins and, with nonces/hashes, which inline scripts may run. You usually want both.

**Should I self-host or use a CDN with SRI?**

Self-host if the file is critical and small. Use a CDN plus integrity when you need the vendor's edge and they publish stable hashes you can update on purpose.

**How does RoastMyOpsec flag CDN risk?**

It reads public HTML tags for cross-origin scripts without integrity and for explicit legacy jQuery version patterns. No payloads, no supply-chain attacks against the CDN.

---

## Spring Actuator vs Apache server-status on Production

- URL: https://roastmyopsec.com/blog/spring-actuator-vs-server-status
- Category: Vulnerabilities
- Updated: 2025-12-27

Spring Actuator vs Apache server-status on production is two live diagnostics that must not be world-readable. Actuator endpoints can expose env, beans, heap dumps, and gateway routes depending on what you enabled. mod_status /server-status exposes workers and sometimes request lines. phpinfo() is the PHP cousin. A liveness probe that returns {"status":"UP"} without details is fine. /actuator/env on the marketing host is not. RoastMyOpsec does not currently score /actuator, /server-status, or phpinfo.php. It does not fetch heap dumps or enable management endpoints.

### The practical difference

Actuator is the application. server-status is the HTTP server. Framework DEBUG is every 500. Lock each layer. Disallow in robots.txt only advertises names.

Spring Boot 3 often maps management to a separate port. Keep that port off the public CDN. If it must share 443, authenticate and expose health only.

### How to choose what to expose

Public origin: health without details, or nothing. Management port: localhost, VPN, or mTLS. CDN: deny /actuator and /server-status even if 'the app is internal.' Staging: same rules if the hostname is guessable.

| Surface | Typical leak | Public origin | Takeaway |
| --- | --- | --- | --- |
| /actuator/env / beans | Config and wiring | No | Not a scored roast path; deny anyway |
| /actuator/health | UP/DOWN | Bare status only if needed | No details payload |
| /server-status | Workers, URIs | No | See server-status vs phpinfo |
| DEBUG true | Every error dump | No | See framework debug |

### What the roast can prove

A 404 on /actuator is not proof the management port is closed. Edge deny is cheap. Pair with swagger vs GraphQL if /actuator/openapi is also public, and with exposed .env if the dump named secrets.

### Common mistakes

The first mistake is management.endpoints.web.exposure.include=* on the public host.

The second mistake is health.show-details=always for a prettier status page.

The third mistake is Disallow: /actuator and calling it access control.

### FAQ

**Is /actuator/health a vulnerability?**

A boolean UP is usually fine. Details, env, and dumps are the miss.

**Does RoastMyOpsec fetch Actuator?**

Not as scored paths today. Deny /actuator on the public origin anyway.

**Is this only Spring?**

The class is any management UI: Actuator, server-status, phpinfo, Micrometer dumps. Same deny list.

---

## Soft 404 vs Hard 404 for Sensitive Paths

- URL: https://roastmyopsec.com/blog/soft-404-vs-hard-404-for-sensitive-paths
- Category: Vulnerabilities
- Updated: 2025-12-01

Soft 404 vs hard 404 for sensitive paths is why status codes lie. A hard 404 (or 403/401) with a tiny generic body usually means the file is not there or is denied. A soft 404 is a 200 that returns your marketing HTML for every unknown path — including /.git/HEAD and /.env. Scanners that only check the status line will pass or fail the wrong test. RoastMyOpsec looks for file-specific signatures and soft-404 behavior on bounded path probes. Deny VCS and env paths at the CDN even when the SPA would have rendered a homepage.

### The practical difference

A hard 404 is an honest miss: the origin or edge says not found. A 403 is an honest deny. A 200 with application/json that is actually a key-value env dump is an incident. A 200 with text/html that is your React shell is a routing default, not a treasure chest — unless the body also matches a real file signature.

Redirect-to-home on every unknown path is the same class of confusion as a soft 404.

### How to choose what to configure

At the edge, deny or 404 well-known sensitive prefixes before they hit the SPA. Let the app 200 only for real document routes. Do not use 'always index.html' for /.git, /.env, /backup.zip, or *.map if those files must never exist in production.

| Response | Likely meaning | OPSEC move | Takeaway |
| --- | --- | --- | --- |
| 404 + small generic body | Not present or blocked | Still deny at edge forever | Honest miss |
| 200 + SPA HTML | Soft 404 / client router | Exclude sensitive prefixes from the rewrite | Status 200 is not a file |
| 200 + env/git signature | Real leak | Rotate, deny, incident process | See .git vs .env guide |
| 301/302 to / | Catch-all redirect | Stop treating it as 404 | Same confusion as a soft 404 |

### When a 'clean' scan still needs an edge rule

A marketing SPA can look clean because every probe returns the shell. That is not the same as a WAF deny. Add explicit denies so a future mis-deploy cannot publish .git next to index.html.

Preview deployments that skip those rules are how 'prod is fine' still leaks.

### Common mistakes

The first mistake is trusting curl -I status codes alone.

The second mistake is a single rewrite-to-index for the entire hostname.

The third mistake is blocking in robots.txt and not at the CDN.

### FAQ

**Does a 200 on /.env always mean the file leaked?**

No. On many SPAs it is the homepage HTML. Confirm Content-Type and whether the body looks like env keys versus your app shell.

**Should I still block /.git if I get a soft 404?**

Yes. Edge deny is cheap and survives the next accidental upload.

**How does RoastMyOpsec tell them apart?**

Bounded GETs plus content signatures and soft-404 checks — not status codes alone. No recursive git dump.

---

## sitemap.xml vs robots.txt for Hidden Paths

- URL: https://roastmyopsec.com/blog/sitemap-xml-vs-robots-txt-for-hidden-paths
- Category: Vulnerabilities
- Updated: 2025-11-23

sitemap.xml vs robots.txt for hidden paths is an invitation versus a request. A sitemap is a list of URLs you want crawlers to fetch. robots.txt Disallow asks well-behaved bots not to crawl a prefix — it does not 403 the path, and it advertises names. Admin, backup.zip, dump.sql, .env, and signed query strings belong in neither file. They belong behind auth and CDN denies. RoastMyOpsec reads public robots.txt for spicy Disallow entries. It does not currently score /sitemap.xml, does not crawl every loc as an attack, and does not treat a missing sitemap as a vulnerability.

### The practical difference

robots.txt is crawler etiquette plus an accidental inventory. sitemap.xml is an explicit inventory. Putting /wp-admin in Disallow and /backup.zip in a sitemap is how you publish the treasure map twice.

Search Console and ads will happily ingest a sitemap. So will anyone who fetches /sitemap.xml. That is a public document.

### How to choose what to list

Sitemap: canonical marketing and docs URLs only, HTTPS, no tokens in query strings. robots.txt: allow the public site; do not Disallow every admin path as a security plan — deny those at the edge. Staging hosts: noindex, auth, and no production sitemap that includes them.

| File | Audience | Must not contain | Takeaway |
| --- | --- | --- | --- |
| sitemap.xml | Crawlers you invited | Admin, dumps, signed URLs, preview hosts | Public catalog of public pages |
| robots.txt Disallow | Polite crawlers | A belief that it blocks humans | See Disallow vs access control |
| CDN deny / 403 | Every client | Hope | Actual control |
| security.txt | Researchers | Internal hostnames | Different well-known file |

### What the roast can prove

Spicy robots.txt Disallow lines are findings. A 200 sitemap that lists /login is not scored today — still remove it in your own review. Pair with dump.sql vs backup.zip if archives were listed, and with open redirects vs tokens if loc URLs carry secrets.

### Common mistakes

The first mistake is generating the sitemap from every route including /admin.

The second mistake is putting API keys or signed URLs in sitemap.xml.

The third mistake is Disallow: /backup and a sitemap loc for the same zip.

### FAQ

**Is a public sitemap a vulnerability?**

Not by itself. Listing sensitive or tokenized URLs is. Keep the sitemap to pages you would print on the homepage.

**Does robots.txt hide sitemap URLs?**

No. Disallow does not remove a loc. Crawlers that ignore robots still fetch both files.

**Does RoastMyOpsec fetch sitemap.xml?**

It is not a scored path today. robots.txt spicy Disallow is. Audit sitemap loc entries yourself.

---

## Apache server-status vs phpinfo() on Production

- URL: https://roastmyopsec.com/blog/server-status-vs-phpinfo-on-production
- Category: Vulnerabilities
- Updated: 2025-10-11

Apache server-status vs phpinfo() on production is two operator pages that must not be world-readable. mod_status /server-status lists workers, vhosts, and request lines. phpinfo() lists the PHP runtime. nginx stub_status and Spring Actuator /health-with-details are cousins. None of these are credentials by themselves; all of them shrink reconnaissance. Bind them to localhost or a VPN, or deny the path at the CDN. RoastMyOpsec does not currently score /server-status, /server-info, or stub_status. It does not fetch phpinfo.php as a scored path. It does not enable ExtendedStatus or dump worker tables.

### The practical difference

server-status is the HTTP server's live scoreboard. phpinfo is the language runtime. Framework debug mode is every 500. Treat all three as diagnostics, not marketing.

robots.txt Disallow: /server-status advertises the name. Deny at the edge instead. See Disallow vs access control.

### How to choose a lock

Production: Location deny all for /server-status and /server-info, or listen only on 127.0.0.1. CDN: deny those prefixes even if Apache is 'internal.' phpinfo.php: delete from the artifact. Actuator: expose only health without details on the public origin.

| Path / page | Typical stack | What it often shows | Takeaway |
| --- | --- | --- | --- |
| /server-status | Apache mod_status | Workers, sometimes request URIs | Not a scored roast path; deny anyway |
| /phpinfo.php | PHP | Runtime and path inventory | See phpinfo vs verbose errors |
| stub_status | nginx | Connection counters | Localhost or deny |
| Framework DEBUG | App | Every 500 as a dump | See framework debug mode |

### What the roast can prove

A 404 on /server-status is not proof it is gone on another vhost. Edge deny is cheap. Pair with htaccess vs web.config if the config that enabled status was downloadable, and with robots.txt if Disallow listed the path.

### Common mistakes

The first mistake is ExtendedStatus On with Location /server-status Allow from all.

The second mistake is Disallow: /server-status and calling it a fix.

The third mistake is exposing Actuator env on the same host as checkout.

### FAQ

**Is /server-status a vulnerability?**

On a public origin it is an information leak. Restrict it to localhost or a management network.

**Does RoastMyOpsec fetch server-status?**

Not as a scored path today. Deny it anyway. phpinfo.php is also not a scored path.

**Is stub_status the same as phpinfo?**

No. stub_status is counters. phpinfo is a runtime brochure. Both should stay off the public origin.

---

## robots.txt Disallow vs Access Control for Hidden Paths

- URL: https://roastmyopsec.com/blog/robots-txt-disallow-vs-access-control
- Category: Vulnerabilities
- Updated: 2025-08-04

robots.txt Disallow vs access control is a control-plane mismatch that shows up constantly on marketing sites. Disallow tells well-behaved crawlers not to fetch a path. It does not authenticate users, it does not block attackers, and it often publishes a list of admin, backup, or staging URLs. Real protection is auth, network allowlists, and edge deny rules. RoastMyOpsec fetches /robots.txt and reads Disallow lines; it does not crawl those paths. Juicy Disallows are a disclosure finding, not a proof of compromise.

### The practical difference

robots.txt is a politeness file for crawlers. Search engines that honor it will skip listed paths. Security scanners, browsers, and people will not.

Access control is a decision on the request: 401, 403, SSO, VPN, or a WAF rule. The path can be guessable and still be safe if the control holds.

### How to choose what belongs in robots.txt

Put crawl budget and duplicate-content paths in robots.txt: faceted search junk, infinite calendars, internal search result pages. Do not put /admin, /backup.zip, /.env, or tokenized preview URLs there.

If a path must stay out of Google, noindex plus authentication is stronger than Disallow alone. Disallow can even prevent Google from seeing a noindex tag on that URL.

| Goal | robots.txt | Access control | Takeaway |
| --- | --- | --- | --- |
| Keep a URL out of search | Sometimes, for crawl waste | Does not replace noindex/auth | Use robots for budget, not secrecy |
| Stop strangers reading a file | Does nothing | Required | Deny at origin or CDN |
| Hide an admin panel | Advertises it if Disallowed | SSO, MFA, VPN, rate limits | Guessing is not the main risk — login is |
| Preview deployments | Do not list secret slugs | Auth or IP allowlist | Unlisted URLs still leak via Referer and chats |

### When robots.txt becomes a treasure map

A Disallow for /admin, /internal, /old, /phpmyadmin, or /backup is a tour guide. RoastMyOpsec calls that out because security-through-obscurity fails harder when you publish the map.

A missing robots.txt is usually not critical. A chatty one is worse than a boring one.

### Common mistakes

The first mistake is Disallow: /secret/ and no auth on /secret/.

The second mistake is putting API keys or signed URLs in sitemap.xml.

The third mistake is blocking CSS/JS in robots and wondering why the site renders poorly in search previews.

### FAQ

**Does Disallow in robots.txt hide a page from attackers?**

No. It only advises crawlers. Anyone can still request the URL. Use authentication or edge deny rules.

**Should I Disallow /admin?**

You can, but it does not protect /admin, and it advertises the path. Protect the panel with SSO and MFA; keep robots mundane.

**Is a missing robots.txt a vulnerability?**

Usually no. It is a crawler-guidance gap. Chatty Disallow lists are the OPSEC problem.

---

## requirements.txt vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/requirements-txt-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2025-07-26

requirements.txt vs package.json on production origins is two language manifests that must not sit next to index.html. /package.json donates npm names and versions. /requirements.txt (and Pipfile, pyproject.toml, poetry.lock) donates Python packages and pins — the same inventory class for Django, Flask, and FastAPI shops. Neither is a pentest. Both shrink reconnaissance and feed advisory lookups. A private PyPI mirror or npm registry is the public catalog. The marketing CDN is not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /requirements.txt, does not parse lockfiles on the URL scan, and does not install packages.

### The practical difference

package.json is Node inventory. requirements.txt is a pip constraint list — sometimes a full freeze with hashes, sometimes a loose Django>=4 line. pyproject.toml is the modern cousin. composer.json is the PHP costume. Dockerfile is how the image is built. .env is secrets, not inventory.

A 200 on /requirements.txt is an information leak even when every pin looks boring. Deny the names at the edge.

### How to choose what to publish

CI: fail if requirements.txt, Pipfile, pyproject.toml, package.json, or lockfiles appear in a static marketing artifact. App origins that must serve those files are rare — prefer not. CDN: deny those basenames. Keep Python files in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | npm names and versions | Bounded GET | See public package.json vs OSV |
| /requirements.txt | pip pins | Not scored; deny anyway | Same class |
| /pyproject.toml | Project metadata and deps | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When requirements.txt still wins (off the public origin)

It wins in a private repo and in CI. It never wins as a downloadable object on the marketing bucket. Pair with Dockerfile vs package.json if the same zip also shipped a container recipe.

### What the roast can prove

A reachable /package.json can surface. requirements.txt is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a repo you own — that is a different channel than a URL roast.

### Common mistakes

The first mistake is copying the whole Django project to S3 because collectstatic lived next to requirements.txt.

The second mistake is a pip freeze that includes private index URLs with tokens in the query string.

The third mistake is Disallow: /requirements.txt and calling it access control.

### FAQ

**Is a public requirements.txt a vulnerability?**

It is an information leak. Remove it and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch requirements.txt?**

Not as a scored path today. It does probe /package.json. Deny Python inventory filenames at the edge anyway.

**Does pyproject.toml belong on the CDN?**

No. Keep project metadata in the private repo. Use OSV on a repo you own, not a marketing URL.

---

## Rails master.key vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/rails-master-key-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2025-05-28

Rails master.key vs .env on a production origin is two ways to spill the app's secret material. /.env is plaintext runtime secrets — the roast signatures it. master.key is the key that unwraps Rails encrypted credentials. Serving either next to index.html is the same class of miss. RoastMyOpsec may not fetch master.key today. Still deny that name at the edge. Do not advertise it in robots.txt. The control is a CDN deny, not a crawler suggestion.

### The practical difference

.env is a bag of KEY=value lines. master.key is a small file whose only job is cryptographic. Public bytes of either are a credential incident. Encrypted credentials files without the key are a different problem; this guide is about not hosting the key on a CDN.

Teams copy config/ into static exports, or deploy the Rails tree as if it were a brochure. The production document root is not config/.

### How to choose based on what you shipped

If master.key or .env was public: take the objects off the origin, deny those names at the CDN, rotate the Rails secret and anything the credentials file named, and stop publishing config trees. Inject the key via the platform secret store on app servers, never via a static bucket.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| .env on the origin | Never | Live secrets in text | Incident; roast signatures this path |
| master.key on the origin | Never | Unwraps encrypted credentials | Same class of miss; deny the name |
| master.key as a platform secret | App hosts you control | Do not write it into public/  | Keep it off every CDN object |
| robots.txt Disallow | Not for keys | Publishes the path | Edge deny instead |

### When each secret wins (always off the brochure origin)

.env wins as private runtime config where the framework expects it — on the server. master.key wins as an injected secret on that same server. Neither wins in public/ or in a Jamstack dist folder.

If /.git/HEAD was also public, assume history. Rotate rather than arguing about which copy was current.

### Common mistakes

The first mistake is gitignoring .env and still committing master.key, then syncing the repo to a public bucket.

The second mistake is rotating RAILS_MASTER_KEY in the host env while the old master.key file stays downloadable.

The third mistake is Disallowing /config in robots.txt and calling it access control.

### What a URL roast can prove

The roast signatures /.env and /.env.local. master.key is not a scored path today. Still deny it. Bounded GETs, no decryption walkthrough, no use of leaked keys. Confirm the bucket, not only a soft-404 homepage.

### FAQ

**Is a public master.key a vulnerability?**

Treat it as a credential incident. Remove it, deny the name at the edge, and rotate the Rails secret and related credentials.

**Does RoastMyOpsec fetch master.key?**

Not as a scored path today. It does probe /.env. Deny master.key at the CDN anyway. Same class of miss as /.env.

**Can I keep credentials.yml.enc on the CDN if the key is private?**

The marketing origin still does not need Rails config. Keep the whole credentials pair off public objects.

---

## pubspec.yaml vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/pubspec-yaml-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2025-05-19

pubspec.yaml vs package.json on production origins is two language manifests that must not sit next to index.html. /package.json donates npm names and versions. /pubspec.yaml (and pubspec.lock) donates Dart and Flutter package names, SDK constraints, and pins — the same inventory class for shops that published the project root next to a web build. Neither is a pentest. Both shrink reconnaissance and feed advisory lookups. pub.dev is the public catalog. The marketing CDN is not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /pubspec.yaml, does not parse pubspec.lock on the URL scan, and does not fetch packages.

### The practical difference

package.json is Node inventory. pubspec.yaml is the Dart/Flutter manifest. pubspec.lock is the exact graph. Cargo.toml is Rust. go.mod is Go. .env is secrets.

A 200 on /pubspec.yaml is an information leak even when every dependency looks boring. Deny the names at the edge.

### How to choose what to publish

CI: fail if pubspec.yaml, pubspec.lock, package.json, or lockfiles appear in a static marketing artifact. Flutter web output should be the build/, not the repo root. CDN: deny those basenames. Keep Dart files in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | npm names and versions | Bounded GET | See public package.json vs OSV |
| /pubspec.yaml | Dart/Flutter deps | Not scored; deny anyway | Same class |
| /pubspec.lock | Exact package graph | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When pubspec.yaml still wins (off the public origin)

It wins in a private repo and in CI. It never wins as a downloadable object on the marketing bucket. Pair with Cargo.toml vs package.json if the same zip also shipped a Rust crate.

### What the roast can prove

A reachable /package.json can surface. pubspec.yaml is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a repo you own — that is a different channel than a URL roast.

### Common mistakes

The first mistake is uploading the Flutter project root because flutter build web lived next to pubspec.yaml.

The second mistake is a git or path dependency in a public pubspec that names an internal repo.

The third mistake is Disallow: /pubspec.yaml and calling it access control.

### FAQ

**Is a public pubspec.yaml a vulnerability?**

It is an information leak. Remove it and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch pubspec.yaml?**

Not as a scored path today. It does probe /package.json. Deny Dart inventory filenames at the edge anyway.

**Does pubspec.lock belong on the CDN?**

No. Keep lockfiles in the private repo. Use OSV on a repo you own, not a marketing URL.

---

## Public package.json vs Lockfile Advisories on Production

- URL: https://roastmyopsec.com/blog/public-package-json-vs-lockfile-advisories
- Category: Vulnerabilities
- Updated: 2025-05-11

Public package.json vs lockfile advisories is two ways your dependency story leaks. A 200 on /package.json with a JSON name field is an inventory gift on the marketing origin — versions, scripts, and sometimes private package names. An OSV hit on package-lock.json inside a repo you deepen is a known-advisory queue, not a public file. You can have a locked-down GitHub and still ship package.json next to index.html. RoastMyOpsec's path check looks for a usable package.json signature on a bounded GET. It does not download node_modules or run npm audit against your CDN.

### The practical difference

package.json on the web root is a leftover from a static export or a mis-synced dist folder. Attackers use it as a shopping list; defenders should treat it like robots.txt that lists internal names. The lockfile in git is the exact pins. OSV cares about those pins. The public JSON is the brochure of the same story, often stale or incomplete.

Removing /package.json does not patch CVEs. Patching CVEs does not hide a file you still serve.

### How to choose what to fix first

If the roast shows package.json on the origin: deny that path at the CDN, stop copying the repo root into the deploy bucket, rotate nothing unless secrets were in scripts (they should not be). If deepen shows OSV IDs: triage reachability and bump the lockfile. If both fire, do the public file the same day — it is a one-line edge rule — then schedule the advisory work.

| Signal | Where it lives | What it proves | Takeaway |
| --- | --- | --- | --- |
| 200 /package.json with name | Public origin | Inventory of declared deps | Block at the edge; fix the publish pipeline |
| OSV match on lockfile | Authorized GitHub deepen | Known advisory for a pin | Triage; see the OSV vs URL guide |
| Secret in public JS | Bundled client code | Credential-shaped string shipped | Different finding; rotate |
| package.json in .git on the CDN | VCS dump | Worse than the one file | See exposed .git vs .env |

### What the scanner will not do

No recursive npm install from your site. No exploit of a listed package. Soft-404 HTML that looks like a homepage is not counted as a hit. A missing file is a pass for this check, not a claim your lockfile is clean.

### Common mistakes

The first mistake is 'it is only metadata' while private registry package names sit in dependencies.

The second mistake is blocking package.json and still serving package-lock.json.

The third mistake is treating a public package.json as equivalent to a completed npm audit.

### FAQ

**Is a public package.json a vulnerability?**

It is an information leak. Treat it as a finding. Block it. It is not the same as a remote code execution.

**Should I also block package-lock.json on the site?**

Yes if it is on the origin. Lockfiles belong in git and CI, not the CDN. Deepen the repo for OSV; do not publish the lockfile as a static asset.

**How does RoastMyOpsec detect package.json?**

A bounded GET and a JSON body with a name field — not a full npm parse. No exploit payloads.

---

## Public JavaScript API Keys vs Server Secrets

- URL: https://roastmyopsec.com/blog/public-javascript-api-keys-vs-server-secrets
- Category: Vulnerabilities
- Updated: 2025-05-02

Public JavaScript API keys vs server secrets is a placement rule, not a naming trick. If a value is in HTML, a bundled .js file, or a public env prefixed for the client, it is public — assume it is copied. Server secrets (database URLs, signing keys, admin tokens, private API keys) belong only on the server. Some vendors issue publishable keys that are meant to be public if they are scoped, rate-limited, and domain-locked. RoastMyOpsec scans inline HTML and same-origin scripts for high-confidence secret patterns; it does not log into your APIs.

### The practical difference

A publishable key is designed to identify your project to a vendor from a browser. It should be useless without a server-side counterpart, domain allowlisting, or tight product limits.

A secret key is designed to act as you. If it appears in a client bundle, treat it as stolen until rotated. Minification and 'obscure variable names' are not controls.

### How to choose what may ship to the browser

Ask two questions. Can this value create charge, read private data, or admin the product by itself? If yes, it is a server secret. Does the vendor document it as publishable and offer domain restriction? If yes, it may live in the client with monitoring.

| Value | Client? | Why | Takeaway |
| --- | --- | --- | --- |
| Publishable payments key | Often yes, if vendor-designed | Cannot capture funds alone when used as documented | Still restrict by domain and monitor |
| Secret / restricted API key | Never | Acts as the account | Rotate if it ever shipped |
| Private PEM / JWT signing key | Never | Mints identity | Incident if it hits git or JS |
| Map or analytics write-only token | Sometimes | Depends on quota and referrer locks | Lock by HTTP referrer and cap usage |

### When a 'public' key is still an OPSEC miss

Unscoped cloud keys, webhook secrets, and 'temporary' debug tokens in main.js are not publishable keys. Neither are connection strings with passwords.

If RoastMyOpsec flags a high-confidence pattern, rotate first, then remove it from the bundle and from git history you still control.

### Common mistakes

The first mistake is putting NEXT_PUBLIC_ or VITE_ on a secret so the build will 'just work.'

The second mistake is committing .env because a tutorial did.

The third mistake is rotating the key but leaving the old one valid 'for rollback.'

### FAQ

**Are all API keys in JavaScript vulnerabilities?**

No. Vendor publishable keys are expected in the browser if they are scoped. Unscoped secret keys, PEMs, and database URLs in JS are incident-class.

**Does obfuscating a key in JS protect it?**

No. Anyone can read the downloaded file. Obfuscation is not access control.

**What should I do if a secret shipped in production JS?**

Revoke and rotate it, purge it from the build, check git history, and add a server proxy or a scoped publishable key instead.

---

## Production Source Maps: When They Leak Source and Secrets

- URL: https://roastmyopsec.com/blog/production-source-maps-when-they-leak
- Category: Vulnerabilities
- Updated: 2025-04-24

Production source maps leak when a public .js file points at a reachable .map, or when maps are uploaded next to hashed assets without access control. Maps are a debugging aid: they reconstruct original filenames, comments, and sometimes strings you thought the bundler ate. They are not a remote-code exploit by themselves. They are an intel and secrets-adjacent exposure. RoastMyOpsec looks for sourceMappingURL breadcrumbs and sensitive paths on the public root; keep maps off production or serve them only to authenticated debug sessions.

### The practical difference

Minified JavaScript is still logic, but it slows humans down. A source map gives the original layout back. That helps your team in staging. It also helps anyone reading the production CDN.

Hidden source maps (no sourceMappingURL, maps not uploaded) keep production noisy. Hidden does not mean secret if a predictable URL still serves the file.

### How to choose based on the environment

Local and staging: maps on, error tracking on. Production: maps off at the CDN, or behind auth for a break-glass debug host. If you upload maps to an error product, restrict that project and do not also leave .map public on the site.

| Environment | Source maps | sourceMappingURL | Takeaway |
| --- | --- | --- | --- |
| Local | On | Fine | Speed of debug wins |
| Shared staging | On if the host is gated | Fine behind auth | Do not copy prod DNS to an open staging bucket |
| Production CDN | Off, or auth-only | Omit | Do not ship maps next to index.html |
| Error tracker upload | Private project | Not required on the public file | One private copy is enough |

### When maps become a vulnerability-adjacent miss

Comments, internal URLs, feature flags, and accidentally bundled .env references show up more clearly in maps. Combined with an exposed .git, you get a full reconstruction kit.

Fix by stopping the upload, removing sourceMappingURL from prod bundles, and denying *.map at the edge if anything still answers.

### Common mistakes

The first mistake is 'hidden-source-map' that still writes files to /static.

The second mistake is enabling maps to debug one production incident and leaving them on.

The third mistake is assuming a content hash in the filename makes the map unguessable when the .js file names it in the last line.

### FAQ

**Are production source maps a CVE?**

Usually no. They are an information disclosure. They become urgent when they contain secrets or make a messy codebase trivial to audit for other bugs.

**Can I keep maps for Sentry or similar?**

Yes, if they are uploaded privately to the tracker and not served from the public origin. Do not also leave sourceMappingURL on the public script.

**How does RoastMyOpsec treat maps?**

The free path check looks for sourceMappingURL breadcrumbs and well-known sensitive files. It does not download your entire asset graph.

---

## postMessage vs CORS for Embedded Widgets

- URL: https://roastmyopsec.com/blog/postmessage-vs-cors-for-embeds
- Category: Vulnerabilities
- Updated: 2025-03-21

postMessage vs CORS for embedded widgets is two different cross-origin pipes. CORS decides whether fetch may read another origin's HTTP response. window.postMessage sends a structured clone to another window; CORS headers do not filter it. The OPSEC is targetOrigin (never *) and checking event.origin on the receiver before trusting the data. Tokens, session flags, and PII in a * postMessage are a publish. iframe sandbox and CSP frame-ancestors still matter for who may load whom. RoastMyOpsec does not execute postMessage, does not inspect widget JS for targetOrigin, and does not score missing origin checks. It may read ACAO on HTTP APIs in app mode.

### The practical difference

A checkout iframe that fetch()es your API needs CORS. The same iframe that parent.postMessage({token}) needs targetOrigin: 'https://pay.example' and a receiver that ignores other origins. Mixing them up is how teams 'fix CORS' and still broadcast the session to every opener.

WebSockets are a third pipe — Origin on upgrade, not postMessage. See WebSockets vs CORS.

### How to choose a pipe

Same-origin widgets: skip postMessage; call functions. Cross-origin embeds: postMessage with an exact origin string (scheme-host-port), validate origin on both sides, keep payloads boring (no cookies, no raw cards). Server APIs: CORS allowlist, not postMessage. Do not use * because 'the CDN hostname changes.'

| Pipe | Browser gate | Must check | Takeaway |
| --- | --- | --- | --- |
| fetch / XHR | CORS | ACAO allowlist + auth | See wildcard vs allowlist |
| postMessage | None like CORS | targetOrigin + event.origin | Never * with secrets |
| WebSocket | No CORS on frames | Origin on upgrade | See WebSockets vs CORS |
| iframe sandbox | Child capabilities | Flags on the tag | Does not replace origin checks |

### What the roast can prove

HTTP CORS behavior can surface. Widget postMessage is not scored. Hunt targetOrigin: '*' in your own bundle. Pair with iframe sandbox vs frame-ancestors if the embed is third-party HTML.

### Common mistakes

The first mistake is targetOrigin: '*' 'just for staging' left in production.

The second mistake is trusting event.data without checking event.origin.

The third mistake is putting a session JWT in the message because CORS blocked the cookie.

### FAQ

**Does CORS stop postMessage?**

No. They are different APIs. Lock targetOrigin and event.origin even when CORS is tight.

**Is postMessage * ever acceptable?**

Only for non-sensitive, non-auth payloads you would print on the homepage. Prefer an explicit origin anyway.

**Does RoastMyOpsec inspect postMessage?**

No. It does not run widget scripts or score targetOrigin.

---

## pnpm-lock.yaml vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/pnpm-lock-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2025-03-12

pnpm-lock.yaml vs package.json on a production origin is two Node inventory files that do not belong next to index.html. /package.json is declared dependencies — the roast signatures it. pnpm-lock.yaml is the exact pnpm pin set, often more detailed. Serving either is the same class of miss as a leftover /package.json. RoastMyOpsec may not fetch pnpm-lock.yaml today. Still deny that name at the edge. Do not advertise lockfiles in robots.txt.

### The practical difference

package.json is the short list. pnpm-lock.yaml is the resolved graph. Public pins help strangers prioritize known advisories against your stack. That work belongs in a private repo with Dependabot, not on the CDN.

A lockfile is not a secret by default. It is still not a marketing asset. Scripts and private registry package names make the leak worse. Keep the publish artifact to hashed front-end files.

### How to choose based on what you shipped

CI: fail if package.json or pnpm-lock.yaml appear in the static artifact. CDN: deny those basenames. Advisories: run them on the repo you own, not by publishing the lockfile. If both files are public, remove both the same day.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /package.json | Almost never on the CDN | Names, versions, scripts | Roast signatures this path today |
| /pnpm-lock.yaml | Never on the CDN | Resolved graph as a map | Same class of miss; deny the name |
| Lockfile in private CI | Reproducible installs and alerts | Do not rsync it to the bucket | Keep pins off the origin |
| robots.txt Disallow | Not for lockfiles | Treasure-map line | Edge deny is the control |

### When each file wins (off the brochure origin)

package.json wins in git and on the registry. pnpm-lock.yaml wins in git and CI. Neither wins as a downloadable object on the production document root.

Yarn and npm lockfiles are the same decision in different filenames. Deny the family at the edge rather than playing allowlist bingo in robots.txt.

### Common mistakes

The first mistake is 'pnpm is unusual so nobody will look for the lockfile' on a public URL.

The second mistake is blocking package.json and leaving pnpm-lock.yaml.

The third mistake is Disallowing the lockfile in robots.txt instead of denying it at the CDN.

### What a URL roast can prove

The roast signatures /package.json. pnpm-lock.yaml is not a scored path today. Still deny it. No install from your origin, no exploit of a listed package. A pass on package.json is not a pass on every lockfile name.

### FAQ

**Is a public pnpm-lock.yaml a vulnerability?**

It is an information leak. Remove it and deny the name. It is not the same as a remote exploit, and it is not appropriate on a marketing origin.

**Does RoastMyOpsec fetch pnpm-lock.yaml?**

Not as a scored path today. It does probe /package.json. Deny the pnpm lockfile at the CDN anyway. Same class of miss.

**Is the lockfile a secret?**

Usually it is inventory, not a password. Private registry names and embedded tokens are the exception — treat those as credential incidents.

---

## phpMyAdmin vs /wp-admin on Public Marketing Origins

- URL: https://roastmyopsec.com/blog/phpmyadmin-vs-wp-admin-public-origin
- Category: Vulnerabilities
- Updated: 2025-03-04

phpMyAdmin vs /wp-admin on a public marketing origin is not a style contest. /wp-admin is the CMS staff door — still needs MFA, SSO, and rate limits, but its presence is normal. phpMyAdmin (and cousins like /adminer.php) is a database console. On the public internet it is a leftover that turns a web host into a SQL GUI for whoever finds it. Hide neither as your only control. Put database tools on a VPN or a jump host. RoastMyOpsec uses soft GETs for common admin paths and may notice spicy robots.txt Disallow lines that mention phpmyadmin. It does not log into MySQL, does not brute-force, and does not exploit the panel.

### The practical difference

WordPress admin is an application you meant to run. A database GUI is operations tooling. Marketing sites that 'just needed to inspect a table' leave phpMyAdmin next to the theme. That is a different blast radius than a locked-down wp-admin with MFA.

A 404 on /phpmyadmin is healthy. A 200 login form on /wp-admin is expected. A 200 phpMyAdmin setup page is an incident until it is gone.

### How to choose what belongs on the origin

Keep /wp-admin if you use WordPress; protect identity, not the path name. Remove phpMyAdmin from production images. Shared hosts that install it by default still need it off or firewalled. robots.txt Disallow is an advertisement, not a lock.

| Path class | Normal on public www? | First fix | Takeaway |
| --- | --- | --- | --- |
| /wp-admin | Yes, for WordPress | MFA, rate limits, updates | See admin discoverability guide |
| /phpmyadmin, /adminer.php | No | Remove from the origin; VPN if needed | Database GUI ≠ CMS |
| /administrator (Joomla-style) | Only if that CMS is live | Same identity controls | Inventory, then lock |
| Disallow: /phpmyadmin | A breadcrumb | Delete the app, then the robots line | See robots vs access control |

### What the roast can prove

Status codes on a short list of common admin paths, plus chatty robots.txt. Absence of /phpmyadmin in that list is not a guarantee the panel lives at a unique name. Do not treat a clean roast as a database-hardening letter.

### Common mistakes

The first mistake is MFA on WordPress and phpMyAdmin with the default URL.

The second mistake is IP-allowlisting wp-admin and leaving the DB GUI on 0.0.0.0.

The third mistake is renaming phpMyAdmin to /db-secret as the entire plan.

### FAQ

**Is a public /wp-admin a vulnerability?**

No. It is discoverable by design. The question is MFA, SSO, rate limits, and whether staff tools belong on the internet.

**Is public phpMyAdmin a vulnerability?**

Treat it as a serious exposure. Remove it from the public origin. Access databases through a VPN or approved ops path.

**Does RoastMyOpsec try default phpMyAdmin passwords?**

No. Soft GETs for presence. No credential stuffing, no SQL, no exploit payloads.

---

## phpinfo() vs Verbose API Errors on Production

- URL: https://roastmyopsec.com/blog/phpinfo-vs-verbose-api-errors
- Category: Vulnerabilities
- Updated: 2025-02-23

phpinfo() vs verbose API errors on production is two costumes of the same leak class. phpinfo() prints PHP version, extensions, paths, and often environment-shaped configuration into an HTML page. Verbose API errors print stack traces, SQL fragments, or framework internals into JSON. Both help an operator debug. Both help a stranger map the stack. Disable display_errors and remove diagnostic routes from the document root. RoastMyOpsec does not currently treat /phpinfo.php as a scored path. It may heuristic-scan common API error bodies. It does not enable debug mode, does not send exploit payloads, and does not dump phpinfo.

### The practical difference

phpinfo is a single well-known page someone left in public/. Verbose errors are every 500 that still has APP_DEBUG=true. Server banners (X-Powered-By: PHP/8.x) are a lighter cousin — a version hint without the full dump. See server banners vs stack fingerprinting.

phpinfo is not a credential file, but it often names include_path, session.save_path, and loaded modules. Treat a reachable page as an information leak and delete it.

### How to choose what to shut off

Production: display_errors off, generic client messages, logs on the server. Remove phpinfo.php, test.php, and framework debug bars from the artifact. CDN: deny those names even if CI is perfect. Staging can keep diagnostics behind auth — not on the marketing host.

| Surface | Typical leak | First response | Takeaway |
| --- | --- | --- | --- |
| /phpinfo.php | Runtime and path inventory | Delete; deny the name | Not a scored roast path today |
| JSON 500 with trace | Code paths, sometimes queries | Generic error; log server-side | See verbose API errors |
| X-Powered-By / Server | Product and version hint | Strip at the edge | Fingerprint, not a dump |
| .env on the origin | Live secrets | Rotate; deny | Incident class; not phpinfo |

### What the roast can prove

Verbose API-body heuristics can surface as findings. A 404 on /phpinfo.php is not a certificate the file is gone under /admin/phpinfo.php. Pair with exposed .git vs .env if the dump named a config path, and with server banners if PHP still announces itself.

### Common mistakes

The first mistake is leaving a phpinfo 'health check' on the same host as checkout.

The second mistake is APP_DEBUG=true because 'it's behind Cloudflare.'

The third mistake is deleting phpinfo.php and keeping display_errors on.

### FAQ

**Is phpinfo a vulnerability?**

It is an information leak. Remove it from production. It is not a password dump by itself.

**Does RoastMyOpsec fetch phpinfo.php?**

Not as a scored path today. Treat it like other diagnostic leftovers and deny the name anyway.

**Is X-Powered-By the same as phpinfo?**

No. A banner is a hint. phpinfo is a brochure. Strip both.

---

## .user.ini vs .htaccess on Production Origins

- URL: https://roastmyopsec.com/blog/php-user-ini-vs-htaccess-on-production
- Category: Vulnerabilities
- Updated: 2025-02-15

.user.ini vs .htaccess on a production origin is two server config files that must not be downloadable next to index.html. .htaccess is Apache per-directory rules. .user.ini is PHP's per-directory INI. Both can name paths, limits, and sometimes credentials or auto_prepend hints. Serving either is the same class of miss as /.env. RoastMyOpsec does not currently score /.user.ini or /.htaccess. It does probe other leftovers. Still deny those names at the edge. Do not Disallow them in robots.txt.

### The practical difference

Apache reads .htaccess when overrides are on. PHP reads .user.ini in the document root and below. Nginx ignores .htaccess but will still serve the file as a public object if the origin has no deny rule. The leak is the text.

Cousins include .htaccess.bak and php.ini souvenirs in zip backups. See dump.sql vs backup.zip and .htaccess vs web.config. The fix is the same: keep server config off the public bucket, deny the names at the CDN.

### How to choose based on what you shipped

CI should fail if .user.ini or .htaccess appear in a static artifact that is not supposed to configure a server. CDN should deny those basenames even on PHP origins so a mis-set MIME type cannot serve them as text. If you need the file for the real origin, it still must not be world-downloadable as a static object.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /.htaccess | Apache origin config, not a CDN object | Downloadable text is a leak | Deny at the edge; not a scored roast path |
| /.user.ini | PHP per-dir settings on the app host | Same if GET returns the file | Same class of miss as /.htaccess |
| /.env | Never on the CDN | Live secrets | Roast signatures this path today |
| robots.txt Disallow | Not for server config | Advertises the filenames | CDN deny is the control |

### When each config file wins (never as a public download)

.htaccess wins as origin configuration Apache actually applies — and even then, prefer server config that is not a downloadable object. .user.ini wins only as PHP runtime config on the app host with the same download deny. Neither wins in an S3 brochure bucket.

A 404 on /.htaccess is not proof /.user.ini is gone. Deny the family. Pair with directory listing if autoindex would have shown the names.

### Common mistakes

The first mistake is uploading the PHP tree to object storage including .user.ini and .htaccess.

The second mistake is gitignoring .env and still committing .user.ini with a path or credential-shaped value.

The third mistake is Disallowing those names in robots.txt and calling it access control.

### What a URL roast can prove

The roast does not currently score /.user.ini or /.htaccess. It signatures /.env, /.git/HEAD, /package.json, and some backup/.DS_Store class probes. Still deny the PHP and Apache config names. Bounded GETs only. No php.ini parse as an exploit recipe. Confirm the bucket, not only a soft-404.

### FAQ

**Is a downloadable .user.ini a vulnerability?**

It is an information leak. Remove it from public objects. If it contained secrets, rotate them. Deny the name at the CDN.

**Does RoastMyOpsec fetch .user.ini or .htaccess?**

Not as scored paths today. It does probe other well-known leftovers. Deny these names anyway. Same class of miss as /.env.

**Do I need .user.ini on a CDN-only site?**

Usually no. PHP INI belongs on the app origin that runs PHP, and even there it must not be a world-readable static file.

---

## Open Redirects vs Tokens in URLs

- URL: https://roastmyopsec.com/blog/open-redirects-vs-tokens-in-urls
- Category: Vulnerabilities
- Updated: 2024-11-22

Open redirects vs tokens in URLs are two ways a public site donates trust. An open redirect lets a stranger pick the next location on your hostname. Tokens in query strings (reset links, OAuth codes, session ids) show up in logs, history, and Referer. Together they are how a magic link walks off-site. RoastMyOpsec follows a bounded public redirect chain, compares registrable domains, and inspects parameter names while redacting values. It does not steal sessions or fuzz open-redirect payloads.

### The practical difference

A redirect is a Location header (or meta/JS hop). Same-site HTTPS to HTTPS with a short chain is normal. A hop to a different registrable domain is a new trust boundary. An unvalidated next= parameter is how that boundary becomes attacker-controlled.

A token in the URL is a credential in a place browsers treat as less secret than a cookie. Even with a tight Referrer-Policy, your CDN logs still saw it.

### How to choose a fix order

Ban open redirects: allowlists of paths on your origin, never a free-form URL. Put secrets in POST bodies, HttpOnly cookies, or one-time server-side stores — not ?token=. Collapse marketing redirect chains so ads do not bounce through three trackers.

| Pattern | Risk | First move | Takeaway |
| --- | --- | --- | --- |
| http → https on the same host | First-hop cleartext | Keep it; add HSTS | See the HSTS vs redirect guide |
| next=https://evil.example | Phishing on your hostname | Allowlist relative paths only | Open redirect is the miss |
| ?reset= or ?code= in GET | Logs, Referer, history | One-time store; short TTL | Do not put vault keys in the address bar |
| OAuth callback with extra hops | Code leakage | Exact redirect URI; no third-party bounce | Register the URI at the IdP |

### When a long chain still fails OPSEC

Campaign tools love redirector hosts. Each hop is another log. If a token rides along, every hop is a leak. Strip query strings before leaving your origin.

Pair this with the Referrer-Policy guide: policy reduces outbound Referer; it does not fix your own query strings.

### Common mistakes

The first mistake is decode-then-redirect on a user-supplied URL.

The second mistake is putting the session in the fragment and then a hop that drops the fragment into a query.

The third mistake is collapsing HTTPS redirects but leaving a tracking domain in the middle.

### FAQ

**Is every redirect a vulnerability?**

No. Same-site HTTPS upgrades and short canonical hops are fine. The miss is an unvalidated destination or a credential in the URL.

**Are password-reset links in email unsafe?**

They are a common pattern. Keep them single-use, short-lived, and do not bounce them through third-party redirectors. Prefer a page that does not echo the token in Referer to other origins.

**How does RoastMyOpsec inspect redirects?**

It follows public redirects within bounds, compares public-suffix domains, and looks at parameter names with values redacted. No session theft.

---

## .npmrc vs an npm Token on Production Origins

- URL: https://roastmyopsec.com/blog/npmrc-vs-npm-token-on-production
- Category: Vulnerabilities
- Updated: 2024-10-27

.npmrc vs an npm token on a production origin is a leftover config file versus the credential that file often carries. A reachable /.npmrc is the same class of miss as /.env or /package.json: the marketing CDN served a build artifact that was never meant for browsers. The token belongs in a private CI secret or a server-only env, not in a public object. RoastMyOpsec currently signatures /.env, /.git/HEAD, and /package.json. It may not fetch /.npmrc today. Still deny the name at the edge. Do not list it in robots.txt — that advertises the path instead of closing it.

### The practical difference

.npmrc is npm's per-project or per-user config: registry maps, scopes, and sometimes an _authToken line. An npm token is the secret that authenticates to a private registry. Teams copy the project folder into a static bucket and both hitchhike onto the origin.

A token in a public .env is already incident-class — the roast signatures that path. A token living only inside .npmrc is the same incident if the file is downloadable. A token-free .npmrc that only names a private registry is still an inventory leak. None of these files belong next to index.html.

### How to choose based on what you shipped

If you shipped a static export or a repo-root sync to the CDN: fail CI when .npmrc appears in the artifact, deny the basename at the edge, and rotate any registry token that might have been in that tree. Keep registry mapping in private CI. Do not Disallow the name in robots.txt as if crawlers were the only readers.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Project .npmrc (no token) | Private repo / CI, registry scope only | Still names private registries if public | Never on the CDN; deny the name |
| npm token in server env | CI and app servers you control | A public /.env is a live leak | Keep off the origin; roast signatures .env |
| .npmrc on the marketing origin | Never | Same class as /.env or /package.json | Incident if it held a token; deny anyway |
| robots.txt Disallow | Crawl budget, not secrets | Publishes the path | Access control is the CDN deny |

### When each option wins (never as a public object)

A token-free .npmrc wins in a private repository so CI hits the right registry. An npm token wins as a short-lived CI secret injected at runtime. Neither wins as a downloadable file on the production document root.

If the same publish also left /package.json or /.env, fix the pipeline once. Staging hostnames that are guessable get the same edge deny.

### Common mistakes

The first mistake is committing a user-level .npmrc that still has an _authToken and then syncing the repo root to S3.

The second mistake is rotating the npm token but leaving the old .npmrc on the CDN 'until the next deploy.'

The third mistake is adding the filename to robots.txt Disallow and calling the leak closed.

### What a URL roast can prove

RoastMyOpsec uses bounded GETs to well-known leftovers and checks file signatures. It signatures /.env, /.env.local, /.git/HEAD, /package.json, and some backup/.DS_Store class paths. It may not fetch /.npmrc today. A clean roast is not proof the file is absent under another prefix. Deny the name at the CDN anyway. No exploit payload, no token use.

### FAQ

**Is a public .npmrc a vulnerability?**

It is an information leak and, if it held a registry token, a credential incident. Remove it from public objects, deny the name at the edge, and rotate anything that may have been inside.

**Does RoastMyOpsec fetch .npmrc?**

Not as a scored path today. The roast does probe /.env and /package.json. Still deny .npmrc at the CDN. It is the same class of miss.

**Should I put the npm token in a public .env instead?**

No. Server-only env on hosts you control. A reachable /.env is incident-class. The roast signatures that path.

---

## Next.js .env.local vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/next-env-local-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2024-10-10

Next.js .env.local vs .env on a production origin is two dotenv files that must not be public objects. Next.js loads .env.local as a local override — teams still copy it into the static export. /.env is the shared file. Both can hold DATABASE_URL, signing secrets, and server-only keys. A reachable file is the same class of miss either way. RoastMyOpsec signatures /.env and /.env.local with bounded GETs looking for KEY=value shape. Deny those names at the edge even after a pass. Do not Disallow them in robots.txt; that is a map, not a lock.

### The practical difference

.env is the shared template-plus-values file people forget to gitignore. .env.local is supposed to stay on a laptop. Production CDNs see them as two paths to the same leak class.

NEXT_PUBLIC_ values belong in the client by design — that is a placement rule, not a reason to publish the whole dotenv file. Server-only names in a public .env.local are still secrets. See public JavaScript API keys vs server secrets for the browser half.

### How to choose based on what you shipped

Fail CI if .env* files appear in the marketing artifact. Deny those names at the CDN on every hostname you ship, including previews. If a hit already happened, rotate what the file named. Keep examples in the private repo, not on the origin — see .env.example vs .env.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /.env | Never on the CDN | Shared secrets in text | Roast signatures this path |
| /.env.local (Next.js) | Never on the CDN | Laptop overrides that hitchhiked | Roast signatures this path too |
| Platform env / server secrets | Runtime on hosts you control | Do not echo them into public JS | NEXT_PUBLIC_ is the only client bag |
| robots.txt Disallow | Not for dotenv | Advertises the files | Edge deny is the control |

### When each dotenv file wins (off the public origin)

.env wins in private deployment config or as a checked-in example with empty values only. .env.local wins on a developer machine. Neither wins in an out/ or dist/ folder that the CDN serves.

A 200 with your SPA shell is a soft-404, not a miss. Grade the body, then deny the path anyway.

### Common mistakes

The first mistake is copying .env.local into the Docker build context and then publishing that context as static files.

The second mistake is gitignoring .env.local locally and still uploading it with a manual drag-and-drop to the bucket.

The third mistake is Disallowing /.env.local in robots.txt instead of blocking it at the CDN.

### What a URL roast can prove

Bounded GETs to /.env and /.env.local with a coarse KEY=value signature. No dotenv parse into a loot list, no use of leaked keys. A pass means those well-known paths did not look like real files — not that every prefix is clean. Keep denying the names.

### FAQ

**Is a public .env.local a vulnerability?**

Yes — treat it like a public .env. Remove it, deny the name at the edge, and rotate secrets it may have held.

**Does RoastMyOpsec fetch .env.local?**

Yes. /.env and /.env.local are scored leftover paths today. Still deny them at the CDN after a pass so the next upload cannot land.

**Are NEXT_PUBLIC_ variables the same as a public .env.local?**

No. NEXT_PUBLIC_ is an intentional client prefix. Publishing the whole .env.local file still leaks server-only names. Keep that file off the origin.

---

## kubeconfig vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/kubeconfig-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2024-07-25

kubeconfig vs .env on a production origin is two secret stores that must never be public objects. /.env is live app credentials — the roast already signatures that path. A reachable kubeconfig is cluster-access material: server URLs, certificate data, and often a user token. Serving it next to index.html is the same class of miss as /.env. RoastMyOpsec may not fetch kubeconfig today. Still deny the name at the edge. Do not put it in robots.txt Disallow; that is a map, not a lock.

### The practical difference

.env is what the web app loads at boot. kubeconfig is what kubectl and controllers use to talk to a cluster. Different runtimes, same rule: the CDN is not a home folder.

A 'redacted' kubeconfig that still names the API server and context is an inventory leak. A full file is a credential incident. Treat a public hit as rotate-and-deny, not as a debate about YAML.

### How to choose based on what you shipped

If the file landed on the origin: take it off the bucket, deny the basename at the CDN, rotate cluster credentials that may have been in that tree, and stop copying operator laptops into static deploys. CI should fail if kubeconfig-shaped names appear in the marketing artifact.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| .env on the origin | Never | Live app secrets | Incident; roast signatures this path |
| kubeconfig on the origin | Never | Cluster access material | Same class of miss; deny the name |
| kubeconfig on a laptop / CI secret | Private admin paths you control | Do not zip it into the web root | Keep off every public object store |
| robots.txt Disallow | Crawl waste only | Advertises the filename | Edge deny, not a robots line |

### When each store wins (always off the CDN)

.env wins as a server-only file or injected platform secret for the app. kubeconfig wins as a tightly held admin file or cloud identity, never as a static asset. Neither wins on a brochure origin.

Preview deployments and 'temporary' buckets get the same rule. A 404 on /.env does not prove kubeconfig is gone.

### Common mistakes

The first mistake is dropping a kubeconfig into the frontend repo 'so local k8s demos work' and then publishing that repo as the site.

The second mistake is rotating app .env values and ignoring cluster credentials because 'the roast did not mention kubeconfig.'

The third mistake is listing the filename in robots.txt instead of denying it at the CDN.

### What a URL roast can prove

RoastMyOpsec signatures /.env and /.env.local with bounded GETs. kubeconfig is not a scored path today. Still deny it. A clean check is not a guarantee a uniquely named config is absent. No cluster API calls, no credential use.

### FAQ

**Is a public kubeconfig a vulnerability?**

Yes — treat it as a credential-adjacent exposure. Remove it, deny the name at the edge, and rotate cluster access that may have been in the file.

**Does RoastMyOpsec fetch kubeconfig?**

Not as a scored path today. It does probe /.env. Deny kubeconfig at the CDN anyway. Same class of miss as /.env.

**Can I keep a 'stripped' kubeconfig on the marketing site?**

No. Cluster endpoints and context names are still a map. Public origins do not host admin files.

---

## JSONP vs CORS for Public Browser APIs

- URL: https://roastmyopsec.com/blog/jsonp-vs-cors-for-public-apis
- Category: Vulnerabilities
- Updated: 2024-07-17

JSONP vs CORS for public browser APIs is a legacy script tag versus Fetch CORS. JSONP wraps JSON in a caller-chosen function name so a <script src> can read another origin. That skips Access-Control-Allow-Origin entirely. Any page can load the URL. Cookies on a simple GET, secrets in the JSON, and a guessable callback name all become the other site's data. CORS lets you name origins, methods, and credentials on purpose. Wildcard plus cookies is still wrong — see credentials vs wildcard. RoastMyOpsec app mode sends a bounded untrusted Origin and reads ACAO. It does not execute JSONP callbacks, does not pad callback names, and does not steal sessions.

### The practical difference

CORS is a browser-enforced read policy on XHR/fetch. JSONP is 'this URL is a JavaScript program.' Search engines and <script> tags do not honor ACAO. If the body is user data or an API key, JSONP is a publish.

A callback query parameter is the tell. So is application/javascript that starts with foo({"ok":true}). Content-Type JSON plus CORS is the replacement.

### How to choose a replacement

Public, non-sensitive reads: CORS * or a static allowlist, no cookies. Logged-in browser clients: named origin, credentials only if you must, SameSite on the cookie. Server-to-server: no JSONP, no CORS — use keys on the server. Do not add JSONP 'for old IE.'

| Pattern | Origin control | Cookie risk | Takeaway |
| --- | --- | --- | --- |
| JSONP ?callback= | None (script tag) | Simple GETs may send cookies | Retire it |
| CORS named ACAO | Explicit origin | Only with credentials true | Default for SPAs |
| CORS * | Every origin may read | Illegal with credentials | Public non-secret only |
| Server proxy | Your origin only | Your session stays first-party | Best for secrets |

### What the roast can prove

A roast can show ACAO reflecting an untrusted Origin. It will not fetch ?callback=test or treat JSONP as a scored path. Hunt callback parameters in your own API docs and gateway. Pair with public JavaScript keys if the JSONP URL embeds a publishable token.

### Common mistakes

The first mistake is leaving JSONP 'just for the marketing widget' on the same host as the account API.

The second mistake is reflecting the callback name without an allowlist of function identifiers — still retire JSONP rather than 'hardening' it.

The third mistake is adding CORS * and JSONP together and calling the stack modern.

### FAQ

**Is JSONP a vulnerability by itself?**

It is a missing origin policy. If the payload is public weather, the risk is low. If it is user or account data, treat it as an exposure.

**Can I lock JSONP to one site?**

Not reliably. Script tags are not CORS. Move to CORS or a first-party proxy.

**Does RoastMyOpsec detect JSONP?**

No. It does not execute callbacks. Check CORS headers and remove callback routes in your own review.

---

## id_rsa vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/id-rsa-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2024-05-27

id_rsa vs .env on a production origin is a private SSH key versus dotenv secrets, both sitting where only index.html should live. A reachable /.env is a credential dump — the roast signatures it. A reachable private key file is the same class of miss: material that authenticates as you. RoastMyOpsec may not fetch id_rsa today. Still deny that name at the edge. Do not list private-key filenames in robots.txt. Access control is a CDN deny, not a crawler hint.

### The practical difference

.env holds application secrets. id_rsa (and cousins such as id_ed25519 without the .pub suffix) is a key pair's private half. Public keys are meant to be public. Private keys are not. The CDN cannot tell the difference unless you keep the private half off the bucket.

If a private key was public, treat it as stolen: remove the object, deny the name, replace the key on systems that trusted it. This guide does not describe using the key. It describes not hosting it.

### How to choose based on what you shipped

Strip SSH key files from static artifacts in CI. Deny private-key basenames at the CDN. Rotate any key that may have been in a public object. Keep deploy keys in a secret manager, not in the web root. Pair with /.git/HEAD if the whole working copy shipped.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| .env on the origin | Never | Live app secrets | Incident; roast signatures this path |
| Private key on the origin | Never | Authenticates as a machine or human | Same class of miss; deny the name |
| Public key (.pub) in docs | Sometimes, if you meant to publish it | Do not upload the private half beside it | Still skip the marketing bucket if unused |
| robots.txt Disallow | Not a vault | Names the file | Deny at the edge instead |

### When each secret store wins (off the public origin)

.env wins as private runtime config. A private SSH key wins in an agent or hardware-backed store on a machine you control. Neither wins as a static download.

Deploy keys for CI belong in the CI secret store. Copying them 'next to the site so the build can git pull' is how they become public.

### Common mistakes

The first mistake is committing a deploy key into the frontend repo and publishing that repo as the CDN root.

The second mistake is deleting id_rsa from the bucket but leaving authorized_keys trust in place without replacing the key.

The third mistake is Disallowing the filename in robots.txt and assuming strangers will not request it.

### What a URL roast can prove

The roast signatures /.env, /.git/HEAD, and a short leftover list. id_rsa is not a scored path today. Still deny it. Bounded GETs only. No key parsing as an attack recipe, no SSH attempts. Confirm absence in the bucket, not only from a homepage 404.

### FAQ

**Is a public id_rsa a vulnerability?**

Yes — treat it as a credential incident. Remove the object, deny the name at the edge, and replace the key wherever it was trusted.

**Does RoastMyOpsec fetch id_rsa?**

Not as a scored path today. It does probe /.env and /.git/HEAD. Deny private-key names at the CDN anyway. Same class of miss.

**Is the .pub file safe on the origin?**

It is not a private key, but the marketing origin still does not need it. Never place the private half beside it.

---

## HTTP TRACE vs Least-Privilege Methods for Public APIs

- URL: https://roastmyopsec.com/blog/http-trace-vs-least-privilege-methods
- Category: Vulnerabilities
- Updated: 2024-04-23

HTTP TRACE vs least-privilege methods is a verb-surface choice. TRACE (and the old TRACK alias) should not be on the menu for a public origin. OPTIONS may advertise Allow or Access-Control-Allow-Methods; listing PUT/DELETE/TRACE on routes that only GET is extra surface. Least privilege means each path accepts only the verbs it implements. RoastMyOpsec's app-mode check reads an OPTIONS response — it does not send TRACE bodies or Cross-Site Tracing exploits.

### The practical difference

GET, POST, PUT, PATCH, DELETE, and OPTIONS are the usual working set. TRACE asks the server to echo the request, which historically collided with browser cookie and header leakage stories. TRACK is a non-standard cousin some stacks still mention.

Advertising a verb and implementing it are different. An Allow: TRACE line is still a signal you should turn off at the proxy even if the handler is a 405.

### How to choose what to allow

Start from the route. A marketing page is GET (and maybe HEAD). A JSON collection might be GET plus POST. Admin mutations should not appear on the public OPTIONS of the brochure host.

| Verb | Typical public use | Default | Takeaway |
| --- | --- | --- | --- |
| GET / HEAD | Read documents and APIs | On for public GETs | Still rate-limit abusive clients |
| POST / PUT / PATCH / DELETE | State changes | Only on routes that mutate | Pair with auth and CSRF strategy |
| OPTIONS | CORS preflight | On if browsers call the API | Do not use it as a TRACE substitute |
| TRACE / TRACK | None for production apps | Off at the edge | Classic footgun — disable it |

### When a 405 is not enough

Some frameworks still echo TRACE through a generic handler. Disable the method at nginx, the load balancer, or the platform so the request never reaches the app.

CORS Access-Control-Allow-Methods should match reality. Advertising DELETE to the world on a read-only resource is a map, not a feature.

### Common mistakes

The first mistake is copying a wildcard Allow from a tutorial.

The second mistake is disabling TRACE in prod and leaving it on a preview host with real cookies.

The third mistake is treating OPTIONS as optional security theater and ignoring what it lists.

### FAQ

**Is HTTP TRACE a vulnerability by itself?**

It is a dangerous default. Disable it. Cross-site tracing is a historical class of issues; you do not need TRACE in production.

**Should I disable OPTIONS too?**

Not if browsers need CORS preflight. Keep OPTIONS, keep the Allow list tight, and still disable TRACE.

**How does RoastMyOpsec check methods?**

An OPTIONS probe that reads advertised methods. No TRACE payloads, no Cross-Site Tracing tests.

---

## .htaccess vs web.config on a Public Origin

- URL: https://roastmyopsec.com/blog/htaccess-vs-web-config-on-production
- Category: Vulnerabilities
- Updated: 2024-04-06

.htaccess vs web.config on a public origin is Apache per-directory config versus IIS/ASP.NET config sitting in the document root. Both are meant for the server, not for browsers. If GET /.htaccess or /web.config returns the file, you donated rewrite maps, directory rules, and sometimes connection strings or module lists. Static hosts and CDNs that treat every path as a public object are how this happens. RoastMyOpsec does not currently score /.htaccess or /web.config. It does probe other well-known leftovers (.env, .git/HEAD, dump.sql). Deny these names at the edge anyway. It does not parse config files or use them as an exploit recipe.

### The practical difference

Apache reads .htaccess when AllowOverride is on. IIS reads web.config. Nginx does not use .htaccess; a copied file is still a public object if the origin serves it. The leak is the text, not which server you run.

Cousins: web.config.bak, .htaccess.bak, applicationHost.config souvenirs in zip backups. See dump.sql vs backup.zip.

### How to choose a deny list

CI: fail if .htaccess or web.config appear in a static artifact that is not supposed to configure a server (Jamstack dist/). CDN: deny those basenames even on Apache origins so a mis-set MIME type cannot serve them as text/plain. If you need the file for the origin, keep it off the public bucket.

| File | Typical stack | What a GET can show | Takeaway |
| --- | --- | --- | --- |
| /.htaccess | Apache | Rewrites, denies, sometimes auth hints | Deny at CDN; not a scored roast path |
| /web.config | IIS / ASP.NET | Modules, sometimes connection strings | Same class; strip from static buckets |
| /.env | Any | Live secrets | Incident; roast does probe this |
| robots.txt Disallow | Crawlers | Names, not the file bytes | Not access control |

### What the roast can prove

A 404 on /.htaccess is not proof it is gone under /blog/.htaccess. Edge deny is cheap. Pair with directory listing if autoindex would have shown the name, and with phpinfo if a PHP dump named the config path.

### Common mistakes

The first mistake is uploading the Apache tree to S3 including .htaccess.

The second mistake is gitignoring .env and committing web.config with a password.

The third mistake is Disallow: /.htaccess and calling it a fix.

### FAQ

**Is a downloadable .htaccess a vulnerability?**

It is an information leak. Remove it from public objects. If it contained secrets, rotate them.

**Does RoastMyOpsec fetch .htaccess or web.config?**

Not as scored paths today. Deny the names anyway. The scan does probe other well-known leftovers.

**Do I need .htaccess on a CDN-only site?**

Usually no. Origin rules belong in the CDN or the real server config, not as a public file.

---

## GraphQL Introspection vs Production APIs

- URL: https://roastmyopsec.com/blog/graphql-introspection-vs-production-apis
- Category: Vulnerabilities
- Updated: 2024-01-29

GraphQL introspection vs production APIs is a schema-publication choice, not an automatic CVE. Introspection lets clients query __schema and learn types, fields, and arguments. That is appropriate for public developer platforms and a miss for private product APIs that never meant to publish the catalog. Turning introspection off does not replace authentication, authorization, or query cost limits. RoastMyOpsec's app-mode check sends one bounded, unauthenticated, read-only introspection POST to /graphql. It does not dump your data or brute-force resolvers.

### The practical difference

A REST OpenAPI file you chose to publish is similar: documentation is not RCE. The OPSEC question is whether strangers should get a complete field list for an authenticated product graph.

Introspection on is convenient for GraphiQL in staging. Production private APIs usually disable it and keep schema docs in an authenticated developer portal.

### How to choose based on the API's job

If the GraphQL endpoint is a public platform with documented types, introspection can stay on behind rate limits. If it backs a logged-in SaaS, disable introspection in production and require auth on every operation. Nested queries still need depth and complexity limits either way.

| API type | Introspection in prod | Still required | Takeaway |
| --- | --- | --- | --- |
| Public developer graph | Often on | Auth where data is private; rate limits | The schema is a product |
| Private SaaS /graphql | Usually off | Authz on every field class | Do not publish the menu |
| Staging only | On behind VPN or basic auth | Do not copy the flag to prod | Same binary, different config |
| Federated graphs | Per subgraph policy | Gateway still needs cost limits | One open subgraph can narrate the rest |

### When disabling introspection is not enough

Field suggestions, error messages, and unauthenticated mutations are separate issues. A locked schema with open resolvers is still a data problem.

Pair production config with boring errors, persisted queries if you use them, and WAF/rate limits on /graphql.

### Common mistakes

The first mistake is leaving the default 'introspection on' from a tutorial.

The second mistake is disabling introspection and calling the API 'private' with no auth on queries.

The third mistake is exposing GraphiQL on the production host.

### FAQ

**Is GraphQL introspection a vulnerability?**

Usually no. It is information disclosure. It becomes urgent when the graph is private and the schema reveals admin-only types you did not intend to advertise.

**Does turning introspection off hide all types?**

No. Clients can still learn from errors, suggested fields, and any types they are allowed to query. Authorization still has to be real.

**How does RoastMyOpsec test GraphQL?**

One bounded unauthenticated POST with a read-only __schema query to /graphql. No mutation payloads, no nested attack queries.

---

## go.mod vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/go-mod-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2024-01-12

go.mod vs package.json on production origins is two language manifests that must not sit next to index.html. /package.json donates npm names and versions. /go.mod (and go.sum) donates module paths, Go version, and require pins — the same inventory class for Go shops that accidentally published the repo root. Neither is a pentest. Both shrink reconnaissance and feed advisory lookups. The public module proxy is the catalog. The marketing CDN is not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /go.mod, does not parse go.sum on the URL scan, and does not download modules.

### The practical difference

package.json is Node inventory. go.mod is the module file. go.sum is the checksum lock. Gemfile is Ruby. requirements.txt is Python. Dockerfile is how the image is built. .env is secrets.

A 200 on /go.mod is an information leak even when every require looks boring. Deny the names at the edge.

### How to choose what to publish

CI: fail if go.mod, go.sum, package.json, or lockfiles appear in a static marketing artifact. App origins that must serve those files are rare — prefer not. CDN: deny those basenames. Keep Go files in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | npm names and versions | Bounded GET | See public package.json vs OSV |
| /go.mod | Module path and requires | Not scored; deny anyway | Same class |
| /go.sum | Checksum graph | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When go.mod still wins (off the public origin)

It wins in a private repo and in CI. It never wins as a downloadable object on the marketing bucket. Pair with Dockerfile vs package.json if the same zip also shipped a container recipe.

### What the roast can prove

A reachable /package.json can surface. go.mod is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a repo you own — that is a different channel than a URL roast.

### Common mistakes

The first mistake is copying the whole Go module to object storage because embed lived next to go.mod.

The second mistake is a replace directive in a public go.mod that names an internal path.

The third mistake is Disallow: /go.mod and calling it access control.

### FAQ

**Is a public go.mod a vulnerability?**

It is an information leak. Remove it and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch go.mod?**

Not as a scored path today. It does probe /package.json. Deny Go inventory filenames at the edge anyway.

**Does go.sum belong on the CDN?**

No. Keep checksum files in the private repo. Use OSV on a repo you own, not a marketing URL.

---

## .gitmodules vs .git/HEAD on Production Origins

- URL: https://roastmyopsec.com/blog/gitmodules-vs-git-head-on-production
- Category: Vulnerabilities
- Updated: 2024-01-03

.gitmodules vs .git/HEAD on a production origin is a submodule inventory file versus proof the git directory is on the CDN. A reachable /.git/HEAD with a ref: signature is a VCS dump class finding — the roast already scores it. A reachable .gitmodules donates submodule URLs and paths, sometimes including private remotes. Serving either is the same class of miss as leftover VCS crumbs next to index.html. RoastMyOpsec may not fetch .gitmodules today. Still deny it at the edge. Do not Disallow git paths in robots.txt; that advertises the working copy.

### The practical difference

.git/HEAD is the pointer into a full history tree if objects are also public. .gitmodules is a small INI map of extra remotes. History is worse. The map is still a map. Neither belongs on a static host.

GitHub Pages documents that submodule URLs in .gitmodules should be https for builds it can fetch — that is a Pages concern, not a reason to serve .gitmodules from your marketing bucket.

### How to choose based on what you shipped

If .git/HEAD hit: deny the .git prefix at the CDN, treat it as a source-and-history incident, rotate secrets that may have been in that repo. If only .gitmodules is in the artifact: still take it off the origin and stop publishing the git working copy. Fail CI when .git or .gitmodules appear in a static export.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /.git/HEAD | Never on the CDN | VCS dump signature | Roast signatures this path today |
| /.gitmodules | Never on the CDN | Submodule URLs and paths | Same class of miss; deny the name |
| Submodules in a private repo | Code sharing you intended | Do not deploy the working copy as the site | Keep git metadata off the origin |
| robots.txt Disallow | Not for .git | Treasure map | Edge deny the prefix |

### When each git file wins (never as a public object)

.git/HEAD wins only inside a real git directory on a machine or in a host you do not publish. .gitmodules wins in a private repository that actually uses submodules. Neither wins next to index.html.

If directory listing is on, the map gets louder. Turn listing off and still deny VCS names at the edge.

### Common mistakes

The first mistake is deploying the git working copy because the build output lived in the same folder.

The second mistake is blocking /.git/HEAD and still serving .gitmodules from the artifact root.

The third mistake is Disallowing /.git in robots.txt and calling the dump closed.

### What a URL roast can prove

Bounded GET to /.git/HEAD looking for a ref: signature. .gitmodules is not a scored path today. Still deny it. No recursive object walk, no history dump, no exploit payload. A miss on HEAD is not proof every git metadata file is gone.

### FAQ

**Is a public .gitmodules a vulnerability?**

It is an information leak and a sign the git tree hitchhiked onto the origin. Remove it, deny git metadata at the edge, and check whether .git/HEAD is also reachable.

**Does RoastMyOpsec fetch .gitmodules?**

Not as a scored path today. It does probe /.git/HEAD. Deny .gitmodules at the CDN anyway. Same class of miss.

**Is .gitmodules worse than .git/HEAD?**

Usually no. HEAD plus objects is the history dump. .gitmodules is the submodule map. Fix both. Do not debate the trophy.

---

## Gemfile vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/gemfile-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2023-12-17

Gemfile vs package.json on production origins is two language manifests that must not sit next to index.html. /package.json donates npm names and versions. /Gemfile (and Gemfile.lock) donates Ruby gems and pins — the same inventory class for Rails, Sinatra, and Jekyll shops. Neither is a pentest. Both shrink reconnaissance and feed advisory lookups. A private gem server or npm registry is the public catalog. The marketing CDN is not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /Gemfile, does not parse lockfiles on the URL scan, and does not install gems.

### The practical difference

package.json is Node inventory. Gemfile is Bundler's constraint list. composer.json is PHP. requirements.txt is Python. Dockerfile is how the image is built. .env is secrets, not inventory.

A 200 on /Gemfile is an information leak even when every gem looks boring. Deny the names at the edge.

### How to choose what to publish

CI: fail if Gemfile, Gemfile.lock, package.json, or other lockfiles appear in a static marketing artifact. App origins that must serve those files are rare — prefer not. CDN: deny those basenames. Keep Ruby files in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | npm names and versions | Bounded GET | See public package.json vs OSV |
| /Gemfile | Ruby gem pins | Not scored; deny anyway | Same class |
| /Gemfile.lock | Exact gem graph | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When a Gemfile still wins (off the public origin)

It wins in a private repo and in CI. It never wins as a downloadable object on the marketing bucket. Pair with requirements.txt vs package.json if the same zip also shipped Python pins.

### What the roast can prove

A reachable /package.json can surface. Gemfile is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a repo you own — that is a different channel than a URL roast.

### Common mistakes

The first mistake is copying the whole Rails tree to S3 because public/ lived next to the Gemfile.

The second mistake is a Gemfile source line with a private index token in the URL.

The third mistake is Disallow: /Gemfile and calling it access control.

### FAQ

**Is a public Gemfile a vulnerability?**

It is an information leak. Remove it and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch Gemfile?**

Not as a scored path today. It does probe /package.json. Deny Ruby inventory filenames at the edge anyway.

**Does Gemfile.lock belong on the CDN?**

No. Keep lockfiles in the private repo. Use OSV on a repo you own, not a marketing URL.

---

## Framework Debug Mode vs Verbose API Errors

- URL: https://roastmyopsec.com/blog/framework-debug-vs-verbose-api-errors
- Category: Vulnerabilities
- Updated: 2023-12-09

Framework debug mode vs verbose API errors is a global switch versus one noisy response. APP_DEBUG=true, Django DEBUG=True, Rails consider_all_requests_local, and similar flags turn 500s into inventory: paths, config keys, sometimes query shapes. A single verbose JSON error without the flag is still a leak, but smaller. phpinfo() is a cousin page dump. Production belongs on generic client messages and server-side logs. RoastMyOpsec may heuristic-scan common API error bodies. It does not set DEBUG, does not POST to trigger exceptions, and does not fetch phpinfo.php as a scored path.

### The practical difference

Debug mode is environment-wide. One forgotten .env on the origin or a CDN that still points at a staging container keeps the yellow page on the brand hostname. Verbose errors can remain after DEBUG is false if a middleware still serializes stack traces. Fix both.

Behind Cloudflare is not off. The edge does not rewrite your framework flag.

### How to choose production settings

CI: fail the build if DEBUG/APP_DEBUG is true in the production artifact. Runtime: generic 4xx/5xx HTML or JSON. Logs: stack traces only on the server. Staging: debug behind auth, not on the marketing host. See verbose API errors and phpinfo guides for the leftover pages.

| Surface | Typical leak | First response | Takeaway |
| --- | --- | --- | --- |
| DEBUG / APP_DEBUG true | Every error becomes a dump | Set false; redeploy | Global switch |
| Chatty JSON 500, debug off | One endpoint's internals | Generic body; log server-side | See verbose API errors |
| /phpinfo.php | Runtime brochure | Delete; deny the name | See phpinfo vs verbose errors |
| X-Powered-By | Version hint | Strip at the edge | Fingerprint, not debug mode |

### What the roast can prove

Heuristic error-body hits can surface. The scan cannot prove DEBUG is false on every route. Confirm the env on the host you submitted. Pair with exposed .env if the flag leaked from a public file.

### Common mistakes

The first mistake is APP_DEBUG=true 'only for this hotfix.'

The second mistake is DEBUG=False in settings.py and True in the container env that wins.

The third mistake is treating a roast miss on error heuristics as proof debug is off.

### FAQ

**Is debug mode a vulnerability?**

On a public origin, yes — it is an information-leak class. Turn it off and keep generic client errors.

**Does Cloudflare hide debug pages?**

No. It proxies whatever your origin returns unless you replace the error page yourself.

**Does RoastMyOpsec set DEBUG?**

No. It does not flip flags or send exploit payloads. It may read public error bodies heuristically.

---

## firebase.json vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/firebase-json-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2023-11-30

firebase.json vs package.json on production origins is two project files that must not sit next to index.html. /package.json donates npm names and versions. /firebase.json donates Firebase Hosting rewrites, redirects, headers, ignore globs, and sometimes Cloud Function names — a map of how the site is wired. Neither is a pentest. A private repo may keep both. The marketing CDN must not serve the config as a static object. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /firebase.json, does not parse Hosting rules, and does not call Cloud Functions.

### The practical difference

package.json is Node inventory. firebase.json is how Hosting routes, caches, and attaches headers. Dockerfile is how the image is built. .env is secrets. A 200 on /firebase.json is an information leak even when every rewrite looks boring. Deny the name at the edge.

Firebase Hosting still applies the config you deploy. The miss is leaving a copy of that JSON as a downloadable file in public/.

### How to choose what to publish

CI: fail if firebase.json, .firebaserc, package.json, or lockfiles appear in a static marketing artifact that is not the Hosting deploy itself — the live site should not expose the file at a guessable URL. CDN: deny /firebase.json. Keep the config in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | npm names and versions | Bounded GET | See public package.json vs OSV |
| /firebase.json | Rewrites, headers, functions | Not scored; deny anyway | Same class |
| /.firebaserc | Project aliases | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When firebase.json still wins (off the public URL)

It wins in a private repo and in the Firebase CLI deploy. It never wins as a downloadable object next to index.html. Pair with Dockerfile vs package.json if the same zip also shipped a container recipe.

### What the roast can prove

A reachable /package.json can surface. firebase.json is not a scored path. Confirm a 404 yourself. Pair with robots.txt Disallow if someone thought hiding the name was access control.

### Common mistakes

The first mistake is copying the whole Firebase project folder into public/ so Hosting also served firebase.json.

The second mistake is putting API keys or internal function URLs in a public firebase.json.

The third mistake is Disallow: /firebase.json and calling it access control.

### FAQ

**Is a public firebase.json a vulnerability?**

It is an information leak. Remove it from the public tree and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch firebase.json?**

Not as a scored path today. It does probe /package.json. Deny Firebase config filenames at the edge anyway.

**Does Firebase need firebase.json on the live origin?**

No. The CLI deploys the config. Visitors do not need to download it.

---

## Exposed .svn vs .git on Production Websites

- URL: https://roastmyopsec.com/blog/exposed-svn-vs-git-on-production
- Category: Vulnerabilities
- Updated: 2023-10-10

Exposed .svn vs .git on production websites is the same incident class in two version-control costumes. A public /.git/HEAD (and the objects beside it) can donate source, history, and sometimes secrets that were committed. A public /.svn directory is Apache Subversion working-copy metadata — entries, pristine files, wc.db — that can donate the same kind of inventory. Neither belongs next to index.html. robots.txt Disallow is not a lock. RoastMyOpsec probes /.git/HEAD with a bounded GET looking for a signature. It does not currently score /.svn, does not dump repositories, and does not reconstruct working copies.

### The practical difference

Git leftovers show up when a deploy copies the whole repo instead of an export. SVN leftovers show up on older PHP and Java shops that checked out on the server. .env in the document root is a third channel. IDE folders are a fourth. Treat all as publish denylist plus CDN deny.

A 404 on /.svn/entries is not proof the working copy is gone from another prefix or an old wc.db path.

### How to choose what to strip

CI: publish an artifact that never contains .git, .svn, .hg, or .bzr. CDN: deny those prefixes even if CI is perfect. Server: never checkout into the document root. Staging hostnames that are guessable get the same rules. Pair with dump.sql if the same zip also held a database.

| Leftover | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /.git/HEAD | Source and history | Bounded signature GET | Incident-class; deny the tree |
| /.svn/ | Working-copy metadata | Not scored; deny anyway | Same class as git |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |
| /.vscode/ | Editor config | Not scored | See .vscode vs .DS_Store |

### When SVN still wins (off the public origin)

A private Subversion server is fine. A working copy in the web root is not. Git still wins as the common leak in 2026. Missing .svn on a Node site is healthy.

### What the roast can prove

A .git/HEAD signature can surface. .svn is not a scored path. Confirm a deny at the edge. Pair with directory listing if autoindex still lists .svn.

### Common mistakes

The first mistake is stripping only .git because 'we migrated from SVN years ago' while an old vhost still has a checkout.

The second mistake is Disallow: /.svn and calling it access control.

The third mistake is treating a roast miss on .svn as proof the working copy is gone.

### FAQ

**Is a public .svn folder a vulnerability?**

Yes — it is an information-leak class, same family as a public .git. Deny it at the edge and stop deploying working copies.

**Does RoastMyOpsec fetch .svn?**

Not as a scored path today. It does probe .git/HEAD. Deny both prefixes anyway.

**Will the scanner dump my repository?**

No. Bounded GETs and signatures only. No recursive dump, no exploit payload.

---

## Exposed Procfile vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/exposed-procfile-vs-package-json
- Category: Vulnerabilities
- Updated: 2023-10-01

An exposed Procfile vs package.json on a production origin is two process maps that must not sit next to index.html. /package.json donates scripts and dependency names — the roast signatures it. A Procfile donates process types and the commands that start them: web, worker, release. That is not a password file, and it is still the same class of miss as leftover inventory on a CDN. RoastMyOpsec may not fetch /Procfile today. Still deny the name at the edge. Do not Disallow it in robots.txt.

### The practical difference

package.json scripts are the Node-shaped list. Procfile is the dyno-shaped list. Together they describe how the app boots more clearly than a marketing page should.

A release command that points at a migrator or a one-off task is an operational hint. Keep that in the private repo. The brochure origin only needs hashed assets.

### How to choose based on what you shipped

Fail CI if Procfile or package.json appear in a static marketing artifact. Deny those basenames at the CDN. If you already shipped them, remove the objects and fix the publish root so the repo root is not the web root.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /package.json | Almost never on the CDN | Scripts and dependency names | Roast signatures this path today |
| /Procfile | Never on the CDN | Process types and start commands | Same class of miss; deny the name |
| Procfile in private git / PaaS | Platform process formation | Do not copy it into dist/ | Keep off the public bucket |
| robots.txt Disallow | Not for this | Advertises the file | Edge deny instead |

### When each inventory file wins (off the public origin)

package.json wins in the private repo. Procfile wins in the private repo and on the PaaS that reads it at deploy time from git, not from a stranger's GET. Neither wins as a static object beside index.html.

Dockerfile in the same export is the same pipeline bug. Fix the artifact once.

### Common mistakes

The first mistake is deploying the Heroku or Dokku project root as the CDN folder.

The second mistake is calling Procfile 'not a secret' while it names internal workers and release tasks.

The third mistake is Disallowing /Procfile in robots.txt instead of denying it at the CDN.

### What a URL roast can prove

The roast signatures /package.json. Procfile is not a scored path today. Still deny it. Bounded GETs, no process-start attempts, no exploit payload. A pass on package.json is not proof Procfile is absent.

### FAQ

**Is a public Procfile a vulnerability?**

It is an information leak. Remove it from the origin and deny the name. It is not usually a credential dump, and it still does not belong on a marketing CDN.

**Does RoastMyOpsec fetch Procfile?**

Not as a scored path today. It does probe /package.json. Deny Procfile at the CDN anyway. Same class of miss.

**Does the platform need Procfile on the public site?**

No. The platform reads it from the app repo at deploy. Browsers do not need it.

---

## Exposed .hg vs .git on Production Websites

- URL: https://roastmyopsec.com/blog/exposed-hg-vs-git-on-production
- Category: Vulnerabilities
- Updated: 2023-09-23

Exposed .hg vs .git on production websites is the same incident class in two DVCS costumes. A public /.git/HEAD (and the objects beside it) can donate source, history, and sometimes secrets that were committed. A public /.hg directory is Mercurial store metadata — dirstate, store, requires — that can donate the same kind of inventory. Neither belongs next to index.html. robots.txt Disallow is not a lock. RoastMyOpsec probes /.git/HEAD with a bounded GET looking for a signature. It does not currently score /.hg, does not dump repositories, and does not reconstruct working copies.

### The practical difference

Git leftovers show up when a deploy copies the whole repo instead of an export. Mercurial leftovers show up on shops that never migrated, or on a forgotten vhost that still uses hg. .svn is a third VCS costume. .env is a secrets channel. Treat all as publish denylist plus CDN deny.

A 404 on /.hg/requires is not proof the store is gone from another prefix or an old clone path.

### How to choose what to strip

CI: publish an artifact that never contains .git, .hg, .svn, or .bzr. CDN: deny those prefixes even if CI is perfect. Server: never clone into the document root. Staging hostnames that are guessable get the same rules. Pair with dump.sql if the same zip also held a database.

| Leftover | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /.git/HEAD | Source and history | Bounded signature GET | Incident-class; deny the tree |
| /.hg/ | Mercurial store metadata | Not scored; deny anyway | Same class as git |
| /.svn/ | Working-copy metadata | Not scored; deny anyway | See .svn vs .git |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When Mercurial still wins (off the public origin)

A private hg server is fine. A clone in the web root is not. Git still wins as the common leak in 2026. Missing .hg on a Node site is healthy.

### What the roast can prove

A .git/HEAD signature can surface. .hg is not a scored path. Confirm a deny at the edge. Pair with directory listing if autoindex still lists .hg.

### Common mistakes

The first mistake is stripping only .git because 'we use GitHub now' while an old host still has an hg clone.

The second mistake is Disallow: /.hg and calling it access control.

The third mistake is treating a roast miss on .hg as proof the store is gone.

### FAQ

**Is a public .hg folder a vulnerability?**

Yes — it is an information-leak class, same family as a public .git. Deny it at the edge and stop deploying working copies.

**Does RoastMyOpsec fetch .hg?**

Not as a scored path today. It does probe .git/HEAD. Deny both prefixes anyway.

**Will the scanner dump my Mercurial repo?**

No. Bounded GETs and signatures only. No recursive dump, no exploit payload.

---

## Exposed .git vs .env on Production Websites

- URL: https://roastmyopsec.com/blog/exposed-git-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2023-09-14

Exposed .git vs .env on production is not a ranking of which leak is 'less bad.' A reachable .env is usually a credential dump — API keys, database URLs, signing secrets. A reachable .git tree is a source and history dump that often contains those same secrets plus every old one. Both belong behind a deny at the CDN or origin, not behind hope. RoastMyOpsec probes well-known paths with bounded GETs, then checks for real file signatures versus soft-404 HTML that pretends the file is missing.

### The practical difference

.env files are configuration. When they are public, rotation is the first job because the values are live secrets until proven otherwise.

.git directories are process. When they are public, you assume source, commit history, and leftover credentials may be recoverable. Blocking the path is not the whole fix if the repo already left the building.

Backups, .DS_Store, phpinfo, and source maps are cousins of the same class: the production document root is not a home folder.

### How to choose what to fix first

If .env is reachable, rotate everything it named, then deny the path. If .git is reachable, take the site offline-from-VCS at the edge, rotate secrets found in history you control, and confirm CI is not publishing the repo as a static folder.

Do not 'hide' these paths only in robots.txt. That advertises them. Access control belongs at the server and CDN.

| Artifact | Typical blast radius | First response | Takeaway |
| --- | --- | --- | --- |
| .env / .env.local | Live secrets for APIs, DB, signing | Rotate, deny, audit logs | Treat as a credential incident |
| .git/HEAD and objects | Source + history + old secrets | Deny at edge, rotate, review history | History is part of the leak |
| zip / sql / bak dumps | Data and config souvenirs | Remove from origin, deny patterns | Backups do not belong next to index.html |
| package.json on a private app | Dependency and script intel | Decide if the app should be public at all | Not always a secret, still a map |

### When a '404' is not a 404

Some frameworks serve the SPA shell for every path. A 200 with your homepage HTML is a soft 404. Scanners that only check status codes will lie. RoastMyOpsec looks for file-specific signatures and soft-404 behavior so a marketing SPA does not get a fake pass.

Your own check on a site you own should confirm Content-Type and body shape, not just the status line.

### Common mistakes

The first mistake is deploying the git working copy as the web root.

The second mistake is copying .env.example to .env and leaving it in public/. 

The third mistake is blocking /.git in one CDN rule and forgetting preview deployments.

### Defensive scan, then edge deny

Run a RoastMyOpsec free audit on a URL you own. Sensitive-path checks are part of the unlocked pack. If something answers like a real file, fix the edge first, rotate second, then re-scan.

### FAQ

**Is an exposed .git worse than an exposed .env?**

Both are incident-class. .env is usually live credentials. .git is source plus history, which often includes credentials from older commits. Fix both; do not debate the trophy.

**Does a 404 on /.git mean I am safe?**

Not by itself. Soft-404s and redirects to home can mask a miss or a hit. Confirm the body is not your app shell, and deny the path at the edge anyway.

**Will RoastMyOpsec download my git history?**

No. Checks are bounded GETs to well-known paths looking for signatures. There is no recursive dump and no exploit payload.

---

## .env.example vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/env-example-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2023-08-20

.env.example vs .env on production origins is two files that must not sit next to index.html. /.env (and /.env.local) is live credentials: database URLs, API secrets, APP_KEY. /.env.example is supposed to be placeholders so new developers know which names to set. Teams still copy real values into the example, or the example lists enough names to map the stack. Neither belongs in a public object store. RoastMyOpsec probes /.env and /.env.local with bounded GETs looking for signatures. It does not currently score /.env.example, does not parse dotenv into a loot list, and does not use leaked keys.

### The practical difference

.env is the secret store a framework loads at boot. .env.example is documentation that hitchhiked into the static artifact. .env.backup and .env.bak are cousins. Git history of a public /.git is a third channel — see exposed .git vs .env.

A 200 on .env.example is not 'safe because it is an example.' Treat it as an information leak and deny the prefix.

### How to choose what to publish

CI: fail if .env, .env.local, .env.example, or .env.* appear in a CDN artifact (keep examples in the private repo only). CDN: deny those names even if CI is perfect. Docs: describe required variables in an internal wiki, not on the marketing host. Staging hostnames that are guessable get the same deny list.

| File | Intended job | Public origin | Takeaway |
| --- | --- | --- | --- |
| /.env / .env.local | Runtime secrets | Never | Scored paths on the roast |
| /.env.example | Dev template | Never on the CDN | Not scored today; deny anyway |
| /.git/HEAD | VCS | Never | See .git vs .env |
| /package.json | JS inventory | Usually no | See public package.json |

### When .env.example still wins (off the public origin)

It wins in a private repo so onboarding knows variable names with empty or obviously fake values. It never wins as a downloadable object on roastmyopsec.com's customers' CDNs. Pair with dump.sql if the same zip also held a database.

### What the roast can prove

Signature hits on /.env and /.env.local can surface. .env.example is not a scored path. A 404 is not proof the file is gone from another prefix. Confirm in the bucket. Pair with framework debug if APP_DEBUG leaked from the same file.

### Common mistakes

The first mistake is committing .env.example with yesterday's production URL still in it.

The second mistake is allowing .env* on the CDN because 'the example has no secrets.'

The third mistake is Disallow: /.env.example and calling it access control.

### FAQ

**Is .env.example a vulnerability?**

On a public origin it is an information leak. Keep templates in the private repo. It is not the same as a live .env, but it is not harmless.

**Does RoastMyOpsec fetch .env.example?**

Not as a scored path today. It does probe .env and .env.local. Deny the whole .env* family at the edge.

**Should I put real-looking keys in the example so it 'works'?**

No. Use empty values or obvious placeholders. Never copy production strings into a template.

---

## ELMAH vs trace.axd on Production ASP.NET Sites

- URL: https://roastmyopsec.com/blog/elmah-vs-trace-axd-on-production
- Category: Vulnerabilities
- Updated: 2023-08-11

ELMAH vs trace.axd on production ASP.NET sites is two leftover operator UIs. ELMAH's /elmah.axd (and similar handlers) is an error log viewer: exception text, URLs, sometimes cookies or form fields depending on what was logged. ASP.NET tracing's /trace.axd is a request trace viewer enabled when tracing is on in web.config. Neither is a login by itself; both shrink reconnaissance and can donate secrets that ended up in an exception. Production belongs on server-side logs behind the firewall, not a public .axd. RoastMyOpsec does not currently score /elmah.axd or /trace.axd. It does not enable tracing, does not dump ELMAH stores, and does not treat a 404 as proof the handler is gone.

### The practical difference

trace.axd is the framework's built-in request log when tracing is enabled. ELMAH is a package that captures unhandled exceptions into a store and, historically, a browser UI. phpinfo() is the PHP cousin. Spring Actuator is the Java cousin. Framework DEBUG is every 500 without a dedicated page. Lock each layer.

robots.txt Disallow: /elmah.axd advertises the name. Deny at the CDN instead. A downloadable web.config that still has tracing enabled is a second leak — see .htaccess vs web.config.

### How to choose what to expose

Public origin: no ELMAH UI, tracing off, customErrors On or RemoteOnly with generic pages. Logs: file, SQL, or a SIEM you authenticate to — not an .axd on 443. Staging: same deny list if the hostname is guessable. CDN: deny /elmah.axd, /trace.axd, and common handler names even if 'IIS is internal.'

| Surface | Typical leak | Public origin | Takeaway |
| --- | --- | --- | --- |
| /elmah.axd | Exception log, often request data | No | Not a scored roast path; deny anyway |
| /trace.axd | Recent request traces | No | Turn tracing off; deny the path |
| web.config compilation debug | Richer errors, extra surface | No | See framework debug |
| /phpinfo.php /actuator | Runtime / env dumps | No | Same class, other stacks |

### When ELMAH still wins (off the public origin)

ELMAH as a library that writes to a private store still wins for operators. The miss is the public handler. trace.axd never belongs on a marketing host. Pair with server-status vs phpinfo if the same IIS box also publishes operator pages.

### What the roast can prove

A 404 on /elmah.axd is not proof the handler is unregistered. Edge deny is cheap. Pair with framework debug if yellow pages still appear, and with exposed .env if the exception named connection strings.

### Common mistakes

The first mistake is leaving ELMAH's handler mapped 'temporarily' on the brand hostname.

The second mistake is tracing enabled='true' in production web.config because local debugging needed it.

The third mistake is Disallow: /trace.axd and calling it access control.

### FAQ

**Is ELMAH itself a vulnerability?**

Logging exceptions privately is fine. A world-readable error UI on the public origin is the miss.

**Does RoastMyOpsec fetch elmah.axd?**

Not as a scored path today. Deny /elmah.axd and /trace.axd on the public origin anyway.

**Is this only old Web Forms?**

Classic .axd names are Web Forms / System.Web. The class is any diagnostic UI: ELMAH, trace, phpinfo, Actuator. Same deny list.

---

## Exposed dump.sql vs backup.zip on Production

- URL: https://roastmyopsec.com/blog/dump-sql-vs-backup-zip-on-production
- Category: Vulnerabilities
- Updated: 2023-08-03

Exposed dump.sql vs backup.zip on production is the same class of miss in two costumes. A reachable SQL dump is often CREATE/INSERT text — schema and data that never belonged on the CDN. A reachable zip with PK magic is an archive that may hold the dump, the CMS, or .env. Neither is 'just a leftover.' RoastMyOpsec uses bounded GETs to well-known names and checks content signatures (SQL keywords, zip magic). It does not brute-force filenames, unzip archives, or run queries against your database.

### The practical difference

dump.sql is usually a mysqldump- or pg_dump-shaped file someone saved 'for a minute.' backup.zip is a folder they compressed. The zip can hide the sql. The sql can hide passwords in INSERT rows. Treat both as credential-adjacent until proven otherwise — then rotate if they were public.

A 200 with a homepage HTML body is a soft-404, not a dump. Grade the signature, then deny the path anyway so the next upload cannot land.

### How to choose what to do first

If either path hits: take the object off the origin, deny .sql/.zip/.tar backup names at the CDN, rotate database and app secrets that might have been in the file, and stop copying backups into the web root. Put archives in private object storage with no public ACL.

| Object | Signature the roast uses | Usual blast radius | Takeaway |
| --- | --- | --- | --- |
| /dump.sql | SQL-ish keywords in the body | Schema and row data | Delete; rotate DB creds if it was live data |
| /backup.zip | Zip magic bytes | Whatever was zipped | Delete; assume .env until you know |
| Index of /backup | HTML listing | Names of the above | See directory listing guide |
| Soft 404 on those paths | Marketing HTML | Confusing, not a dump | Still deny the prefix at the edge |

### What the scanner will not do

No unzip, no SQL parse beyond a coarse signature, no guessing dump-2024-final-v3.sql. A clean check is not a guarantee a uniquely named archive is absent. CI should fail if backup extensions appear in the publish artifact.

### Common mistakes

The first mistake is 'the zip is encrypted' sitting on a public URL anyway.

The second mistake is dumping to /tmp on the web host and letting the docroot include it.

The third mistake is robots.txt Disallow for /dump.sql as if crawlers were the only readers.

### FAQ

**Is a public SQL dump a vulnerability?**

Yes — it is a data exposure. Remove it, deny the path, and rotate anything that may have been inside.

**If backup.zip is empty of secrets, is it still a finding?**

A public archive on the origin is still an OPSEC miss. Do not host backups next to the marketing site.

**Does RoastMyOpsec download and unzip backup.zip?**

No. Bounded GET plus magic-byte check. No recursive dump, no exploit payload.

---

## .DS_Store vs Directory Listing for Leftovers on Production

- URL: https://roastmyopsec.com/blog/ds-store-vs-directory-listing-on-production
- Category: Vulnerabilities
- Updated: 2023-07-25

.DS_Store vs directory listing is two ways a static folder donates names. A public /.DS_Store (or one under /assets) is Apple Finder metadata that can include filenames from the folder someone synced. An 'Index of /' page is the same idea with HTML. Listing is louder; .DS_Store is the leftover that survives after autoindex is turned off. Neither is access control. RoastMyOpsec probes /.DS_Store with a bounded GET and looks for a known binary signature. It does not parse the file into a treasure map and it does not crawl every subdirectory.

### The practical difference

Directory listing is a web-server feature. .DS_Store is a desktop OS feature that hitchhiked into the deploy tarball. Marketing teams that zip a Mac folder onto S3 ship both unless the pipeline strips dotfiles.

A hit on /.DS_Store is an information leak. It is not proof a backup.zip exists — but it is a reason to deny dotfiles at the CDN and add them to the 'do not publish' list next to .env and .git.

### How to choose what to fix

Strip .DS_Store, Thumbs.db, and .git in the CI publish step. Deny those names at the edge even if CI is perfect. Turn off autoindex. Then look for backup.zip and dump.sql the same week — they are the payload, the metadata was the map.

| Leftover | What it often reveals | Typical cause | Takeaway |
| --- | --- | --- | --- |
| /.DS_Store | Filenames from a Mac folder | Drag-drop deploy, unsanitized zip | Strip in CI; deny at CDN |
| Index of /static | Every object in the prefix | Autoindex left on | See directory listing vs 403 |
| /backup.zip | The archive itself | Temporary upload that stayed | Incident if the signature matches |
| /package.json | Dependency inventory | Repo root copied to dist | See public package.json guide |

### What the roast will not do

No recursive walk of every prefix looking for nested .DS_Store. No guide to decoding Finder records. A short or empty body is not counted as a hit. Soft-404 HTML is not a signature match.

### Common mistakes

The first mistake is deleting the file once and not denying the path, so the next intern zip brings it back.

The second mistake is gitignoring .DS_Store locally and still uploading a zip that contains it.

The third mistake is treating a .DS_Store finding as equal to an exposed .env.

### FAQ

**Is .DS_Store a vulnerability?**

It is an information leak. Remove it and deny the name. It is not the same as a credential dump.

**If listing is off, can .DS_Store still leak names?**

Yes. The file is a separate object. Turning off autoindex does not delete Finder metadata you uploaded.

**How does RoastMyOpsec detect .DS_Store?**

A bounded GET and a binary signature check. No recursive dump, no exploit payload.

---

## .dockerignore vs Dockerfile on Production Origins

- URL: https://roastmyopsec.com/blog/dockerignore-vs-dockerfile-on-production
- Category: Vulnerabilities
- Updated: 2023-07-08

.dockerignore vs Dockerfile on a production origin is two build-context files that must not be public objects. /Dockerfile is the image recipe. /.dockerignore is the list of paths you kept out of the context — often the same family as .env, keys, and node_modules. Serving either next to index.html is the same class of miss as /package.json. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /Dockerfile or /.dockerignore. Still deny those names at the edge. Do not advertise them in robots.txt.

### The practical difference

Dockerfile tells the builder how to assemble the image. .dockerignore tells the builder what not to send. Together they describe your private tree more clearly than a marketing page ever should.

A public ignore file is not 'safe because it only lists exclusions.' It is a map of names you considered sensitive. Keep both files in the private repo. The CDN is not a build context.

### How to choose based on what you shipped

CI should fail if Docker files appear in a static marketing artifact. CDN rules should deny those basenames even when CI is clean. If you already published them, take the objects off the origin and stop copying the repo root into the bucket. Pair with /package.json if the same export shipped Node inventory.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| Dockerfile in a private repo | CI image builds you control | ENV and ARG can still look live | Never as a CDN object |
| .dockerignore in a private repo | Keeps secrets out of the image context | Public copy names what you hid | Deny the name at the edge |
| Either file on the origin | Never | Same class as /package.json | Strip from the publish artifact |
| /package.json on the origin | Almost never | Inventory gift | Roast signatures this path today |

### When each file still wins (off the public origin)

Dockerfile wins in private CI. .dockerignore wins in that same private context so local secrets never enter the image. Neither wins as a downloadable object on the brochure site.

Guessable staging hosts need the same deny list. A 404 on one path is not proof the other file is gone.

### Common mistakes

The first mistake is copying the whole git working tree to the static bucket because 'the site is just HTML.'

The second mistake is treating .dockerignore as harmless metadata while it names .env and key files.

The third mistake is Disallowing Docker filenames in robots.txt instead of denying them at the CDN.

### What a URL roast can prove

The roast signatures /package.json today. Dockerfile and .dockerignore are not scored paths. Confirm in the bucket that they are absent, then deny the names so a future sync cannot republish them. Bounded GETs only. No image pull, no context parse.

### FAQ

**Is a public .dockerignore a vulnerability?**

It is an information leak. It often names the files you tried to keep private. Remove it from the origin and deny the basename.

**Does RoastMyOpsec fetch .dockerignore or Dockerfile?**

Not as scored paths today. It does probe /package.json. Deny Docker filenames at the edge anyway. Same class of miss.

**If the Dockerfile has no secrets, can it stay public?**

Not on the marketing origin. Base images, ports, and COPY paths are still a map. Keep it in the private repo.

---

## Dockerfile vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/dockerfile-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2023-06-30

Dockerfile vs package.json on production origins is two build manifests on a public object store. /package.json donates dependency names and versions — useful for OSV homework, not a pentest. A public /Dockerfile (or docker-compose.yml) donates base images, ports, COPY paths, and sometimes ENV with live-looking secrets someone baked in. Neither belongs next to index.html. A private GitHub repo may keep both. The CDN must not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /Dockerfile, does not parse compose files, and does not pull images.

### The practical difference

package.json is Node inventory. Dockerfile is how the image is built. docker-compose.yml is how services wire together — often with published ports and .env file references. .env.example is a cousin. .git is a third channel if the whole repo shipped.

A 200 on /Dockerfile is an information leak even when every ENV looks like a placeholder. Deny the names at the edge.

### How to choose what to publish

CI: fail if Dockerfile, docker-compose*.yml, package.json, or lockfiles appear in a static marketing artifact. App origins that must serve package.json for a legitimate reason are rare — prefer not. CDN: deny those basenames. Keep Docker files in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | Names and versions | Bounded GET | See public package.json vs OSV |
| /Dockerfile | Base image, ports, ENV | Not scored; deny anyway | Same class |
| /docker-compose.yml | Services and ports | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When a Dockerfile still wins (off the public origin)

It wins in a private repo and in CI. It never wins as a downloadable object on the marketing bucket. Pair with exposed .svn vs .git if the same zip also shipped a working copy.

### What the roast can prove

A reachable /package.json can surface. Dockerfile is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a repo you own — that is a different channel than a URL roast.

### Common mistakes

The first mistake is copying the whole repo to S3 because 'static export' still had Docker files in the folder.

The second mistake is ARG passwords in a public Dockerfile 'because they are build-time.'

The third mistake is Disallow: /Dockerfile and calling it access control.

### FAQ

**Is a public Dockerfile a vulnerability?**

It is an information leak. Remove it and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch Dockerfile?**

Not as a scored path today. It does probe /package.json. Deny Docker filenames at the edge anyway.

**Should package.json stay public for npm?**

The registry is the public catalog. The marketing origin does not need to serve the file.

---

## Directory Listing vs 403 for Backup and Static Folders

- URL: https://roastmyopsec.com/blog/directory-listing-vs-403-for-backup-folders
- Category: Vulnerabilities
- Updated: 2023-06-04

Directory listing vs 403 for backup and static folders is a map-versus-lock decision. Autoindex (an 'Index of /' HTML page) lists every file the web server will serve in that path — backups, old zips, leftover SQL dumps, copy-of-config. A 403 or a generic 404 without a listing does not advertise the names. It still does not make a guessed /backup.zip safe: if the file exists and returns a zip signature, that is an exposure. RoastMyOpsec uses bounded GETs to well-known backup and VCS paths and checks content signatures. It does not crawl directory trees or brute-force filenames.

### The practical difference

A listing is an inventory gift. Anyone who finds /files/ or /backup/ sees the names you thought were obscure. A 403 is an honest deny. A 404 is an honest miss — unless it is a soft-404 SPA shell, which is a different lie (see the soft-404 guide).

Turning off autoindex is necessary and not sufficient. Files you uploaded 'just for a minute' remain downloadable at the exact URL. Block backup extensions and VCS prefixes at the CDN as well.

### How to choose what to configure

Static marketing assets can live in hashed filenames under /assets with listing off. Anything named backup, dump, old, copy, or .zip/.sql/.tar does not belong on the public origin. Put archives in object storage that is not the website, with no public ACL.

If you must keep a private drop folder, it is not a website path. Use signed URLs or a VPN. robots.txt Disallow is not a lock.

| Response | What a stranger learns | What is still true | Takeaway |
| --- | --- | --- | --- |
| Index of /backup | Every filename in the folder | Each file may still download | Disable autoindex; remove the files |
| 403 on /backup/ | The path exists and is denied | Exact file URLs may still 200 | Deny the prefix at the edge |
| 404 generic on /backup.zip | Little | Good if the object is gone | Confirm the body is not a zip |
| 200 with PK zip magic | The archive is public | Listing was optional | Incident: rotate, delete, deny |

### What the roast actually checks

The free path plan looks for common sensitive paths — including backup-shaped names — and usable file signatures (for example a zip magic number). It will not enumerate every object in a bucket. A clean roast is not a guarantee nobody left archive.tar.gz under a unique name.

Pair with exposed .git/.env and soft-404 so a 200 homepage on /backup.zip is not mistaken for a miss.

### Common mistakes

The first mistake is turning off listing and leaving last-week.zip in the same folder.

The second mistake is Disallow: /backup in robots.txt as if crawlers were the threat.

The third mistake is serving user uploads from a world-listable prefix next to the marketing site.

### FAQ

**Is directory listing a vulnerability?**

It is an information leak that often becomes a dump. Treat it as a finding. Remove listings and the sensitive files.

**If listing is off, are backups safe?**

No. Guessable names still download. Keep archives off the public origin and deny backup extensions at the CDN.

**Does RoastMyOpsec spider every folder?**

No. Bounded GETs to well-known paths plus signatures. No recursive listing, no filename brute force.

---

## desktop.ini vs Thumbs.db on Production CDNs

- URL: https://roastmyopsec.com/blog/desktop-ini-vs-thumbs-db-on-production
- Category: Vulnerabilities
- Updated: 2023-05-27

desktop.ini vs Thumbs.db on production CDNs is two Windows leftovers next to index.html. desktop.ini is a folder-customization file (icon, InfoTip, localized name) that can list paths and sometimes point at a .ico you also uploaded. Thumbs.db is a thumbnail cache that can name images from the folder someone zipped. .DS_Store is the Mac cousin. None of them are access control. Autoindex off does not delete them. RoastMyOpsec probes /.DS_Store with a bounded GET and a binary signature. It does not currently score /desktop.ini or /Thumbs.db, does not parse INI or thumbnail databases, and does not crawl every prefix looking for nested leftovers.

### The practical difference

Thumbs.db is a cache of pictures. desktop.ini is Explorer metadata for that folder. Mixed shops ship both plus .DS_Store. The leak is names and sometimes icon paths — not usually the file bytes, but enough to map backup.zip sitting beside them.

CI that only greps .DS_Store still ships Windows junk from a designer zip. Deny all three names at the CDN.

### How to choose what to strip

CI: fail the build if desktop.ini, Thumbs.db, ehthumbs.db, or .DS_Store appear in the artifact. CDN: deny those basenames even if CI is perfect. Do not 'clean once' on a laptop and call it a pipeline. Nested /assets/product/desktop.ini counts.

| Leftover | Typical OS | What it often reveals | Takeaway |
| --- | --- | --- | --- |
| /.DS_Store | macOS | Filenames from Finder | Scored path on the roast |
| /Thumbs.db | Windows | Image names from a folder | Same class; deny anyway |
| /desktop.ini | Windows | Folder icon and names | Not a scored roast path; strip it |
| Index of / | Server feature | The whole tree | See directory listing vs 403 |

### When desktop.ini still belongs (not on the CDN)

A Windows share that users browse in Explorer can keep desktop.ini. A public object store cannot. Pair with dump.sql vs backup.zip if the same zip also held a database.

### What the roast can prove

A .DS_Store signature on a bounded GET can surface. desktop.ini is not scored. Confirm a 404 yourself. Pair with .htaccess vs web.config if the same Windows zip also shipped IIS config.

### Common mistakes

The first mistake is stripping only .DS_Store because the Mac laptops were the known problem.

The second mistake is allowing desktop.ini because 'it is just an INI.'

The third mistake is Disallow: /desktop.ini and calling it access control.

### FAQ

**Is desktop.ini a vulnerability?**

It is an information leak. Remove it and deny the name. It is not a credential dump by itself.

**Does RoastMyOpsec check desktop.ini?**

The bounded path list includes .DS_Store. Treat desktop.ini and Thumbs.db as the same hygiene even if this scan does not score them.

**Will the scanner parse INI files?**

No. Signature checks only. No recursive dump, no exploit payload.

---

## crossdomain.xml vs CORS for Public Browser APIs

- URL: https://roastmyopsec.com/blog/crossdomain-xml-vs-cors-for-public-apis
- Category: Vulnerabilities
- Updated: 2022-10-09

crossdomain.xml vs CORS for public browser APIs is two generations of cross-origin policy. CORS (Access-Control-Allow-Origin and friends) is what browsers enforce on fetch and XHR. /crossdomain.xml is an Adobe Flash (and some PDF/plugin) policy file that listed which origins could talk to the host. A file that still says allow-access-from domain="*" is a leftover allow-all, not a substitute for a named CORS allowlist. Modern Chrome does not run Flash. The file is still an inventory gift and a signal that nobody audited the origin. clientaccesspolicy.xml is the Silverlight cousin. RoastMyOpsec app mode sends a bounded untrusted Origin and reads ACAO. It does not currently score /crossdomain.xml, does not parse policy XML, and does not load plugins.

### The practical difference

CORS is response headers on the API. crossdomain.xml is a well-known XML document at the site root (and sometimes in subpaths). JSONP is a third costume: a script callback with no origin list. Replace JSONP with CORS. Delete Flash policy files.

robots.txt Disallow: /crossdomain.xml advertises the name. Deny or 404 the object at the CDN. See JSONP vs CORS and CORS wildcard vs allowlist.

### How to choose a policy

Public marketing APIs for browsers: CORS with named origins, no credentials unless you mean it. No crossdomain.xml in the artifact. If a legacy PDF or kiosk still required the file, it does not belong on the brand CDN next to index.html — isolate that host. Staging copies of * policies must not ship to production.

| Control | Who honors it | Public origin | Takeaway |
| --- | --- | --- | --- |
| CORS ACAO allowlist | Browsers on fetch/XHR | Named origins | The modern gate |
| /crossdomain.xml * | Legacy plugin policy | Delete | Not a scored roast path; remove anyway |
| JSONP callback= | Any script tag | Retire | See JSONP vs CORS |
| clientaccesspolicy.xml | Silverlight leftover | Delete | Same class |

### When CORS wins

CORS always wins for browser APIs in 2026. crossdomain.xml never wins as a security control on a marketing site. A missing file is healthy. Pair with credentials vs wildcard if ACAO is * with cookies.

### What the roast can prove

ACAO on a bounded untrusted Origin can surface. crossdomain.xml is not a scored path. Confirm a 404 yourself. Pair with public JS keys if the old policy sat beside a publishable key.

### Common mistakes

The first mistake is copying allow-access-from * from a 2012 CMS into the static bucket.

The second mistake is treating a 200 on /crossdomain.xml as 'required for CORS.'

The third mistake is Disallow: /crossdomain.xml and calling it access control.

### FAQ

**Do I need crossdomain.xml for CORS?**

No. CORS is headers. The XML file is a Flash-era leftover. Delete it from the public origin.

**Is a missing crossdomain.xml a roast F?**

No. Missing the file is the healthy state. A world-readable * policy is the miss.

**Does RoastMyOpsec fetch crossdomain.xml?**

Not as a scored path today. It does read CORS headers. Deny or delete the XML anyway.

---

## CORS Wildcard vs Allowlist for Public APIs

- URL: https://roastmyopsec.com/blog/cors-wildcard-vs-allowlist-for-public-apis
- Category: Vulnerabilities
- Updated: 2022-09-30

CORS wildcard vs allowlist is a trust-boundary choice for browser-called APIs, not a performance tweak. Access-Control-Allow-Origin: * is acceptable only for responses that are meant to be read by any website and that do not ride on cookies or client certificates. If the API uses credentials, reflect an exact allowlisted origin or reject the request. Wildcard plus Allow-Credentials is invalid in browsers and is a policy smell even when clients refuse it. RoastMyOpsec's app-mode CORS check sends a bounded GET/OPTIONS with an untrusted Origin — no credential stuffing, no authenticated abuse.

### The practical difference

CORS is a browser rule. Other websites' JavaScript may not read your API response unless you opt in. Non-browser clients ignore CORS entirely, so it is not a substitute for authentication.

A wildcard says 'any origin may read this.' An allowlist says 'only these HTTPS origins may.' Reflecting whatever Origin arrived is usually worse than a static * because it looks like a tailored yes to an attacker site.

### How to choose based on the API

Start with whether the response contains anything a stranger's page should not see. Public marketing JSON can often use *. Account, billing, and admin APIs cannot. If the browser will send cookies, you need Allow-Credentials and a specific origin — never *.

| API job | ACA-Origin | Credentials | Takeaway |
| --- | --- | --- | --- |
| Public, no cookies | * or a short allowlist | Off | Wildcard is a product choice, not a footgun |
| Same-site SPA + API | Exact app origin | On only if you actually use cookies | Prefer same-origin and skip CORS entirely |
| Partner widget | Named partner origins | Usually off; tokens in Authorization instead | Do not copy the partner list onto admin routes |
| Reflect any Origin | Attacker-controlled | Especially bad if on | This is the classic miss |

### When a wildcard still fails OPSEC

Wildcard on a JSON error body that includes stack traces or account ids leaks through any website that can trigger the request. Combine boring errors with a tight origin policy.

If you do not need cross-origin browser reads, omit CORS headers. Absence is safer than a generous default copied from a tutorial.

### Common mistakes

The first mistake is Access-Control-Allow-Origin: * plus Access-Control-Allow-Credentials: true.

The second mistake is echoing the Origin header with no allowlist.

The third mistake is setting CORS on the marketing site and forgetting the API host that actually holds sessions.

### FAQ

**Is Access-Control-Allow-Origin * always a vulnerability?**

No. It is appropriate for public, credential-free resources. It is a miss on APIs that return private data or that expect cookies.

**Does CORS replace API authentication?**

No. CORS only affects browsers. Authenticate and authorize every request as if any client on the internet can call the URL.

**How does RoastMyOpsec test CORS?**

App mode sends a bounded request with an untrusted Origin and reads ACAO / credentials behavior. It does not send your users' cookies or attempt account takeover.

---

## CORS Credentials vs Wildcard Origins

- URL: https://roastmyopsec.com/blog/cors-credentials-vs-wildcard-origins
- Category: Vulnerabilities
- Updated: 2022-09-22

CORS credentials vs wildcard origins is a combination the Fetch spec forbids. If Access-Control-Allow-Credentials is true, Access-Control-Allow-Origin must be an explicit origin, not *. Reflecting any Origin while setting credentials true is the 'dynamic *' miss — you named one origin per response but allowed every caller. Public APIs that do not use cookies should omit credentials and may use * only when the body is meant for the whole web. Cookie or Authorization-header APIs need a strict allowlist. RoastMyOpsec app mode sends a bounded untrusted Origin and reads ACAO / credentials behavior. It does not send your users' cookies or attempt account takeover.

### The practical difference

Credentials mode is whether the browser will attach cookies (and some client certs) on the cross-origin call. Wildcard ACAO is 'anyone may read this.' Together they would mean anyone may read cookie-authenticated JSON — so browsers refuse. Operators who still want 'open CORS plus cookies' often echo Origin. That is not a wildcard in the header string, but it is a wildcard in spirit.

See CORS wildcard vs allowlist for the public-JSON case, and CORP vs CORS for embedding locks.

### How to choose based on the API

Anonymous public JSON: ACAO * , no Allow-Credentials. SPA on https://app.example.com calling https://api.example.com with cookies: ACAO https://app.example.com, Allow-Credentials true, SameSite on those cookies, CSRF still in play for cookie sessions. Do not allow https://evil.example by reflecting Origin.

| Pattern | Browser accepts? | Who can read with cookies? | Takeaway |
| --- | --- | --- | --- |
| ACAO * + credentials true | No | Nobody (failed CORS) | Broken and loud |
| ACAO * + credentials absent | Yes for credentialless | N/A — cookies not sent | OK for truly public bodies |
| Named origin + credentials true | Yes | That origin's pages | Default for cookie APIs |
| Echo any Origin + credentials true | Yes, per request | Whoever called | Treat as wildcard; don't |

### What the roast can see

A hostile Origin and the ACAO / credentials headers on that response. Echoing our test Origin with credentials is a signal. It is not a stolen session. Pair with SameSite and CSRF guides for cookie APIs.

### Common mistakes

The first mistake is * plus credentials because a framework defaulted both.

The second mistake is reflecting Origin with a regex that allows any https subdomain.

The third mistake is Access-Control-Allow-Headers: * with credentials without knowing which headers you meant.

### FAQ

**Why did my CORS request fail with * and credentials?**

Browsers reject that pair. Use a specific origin or drop credentials.

**Is echoing Origin with credentials a vulnerability?**

It is an allow-everyone policy. If the API is authenticated with cookies, that is a serious miss. Allowlist the SPA origin.

**Does RoastMyOpsec use my session cookie in CORS tests?**

No. Bounded Origin header only. No account takeover, no credential stuffing.

---

## CORP vs CORS for Embedding and Fetch Isolation

- URL: https://roastmyopsec.com/blog/corp-vs-cors-for-embedding-isolation
- Category: Vulnerabilities
- Updated: 2022-09-13

CORP vs CORS for embedding and fetch isolation is a deny-by-default versus allow-by-name split. Cross-Origin-Resource-Policy (same-origin or same-site) tells browsers to refuse this response as a cross-origin no-cors subresource — images, scripts, and fetches that would otherwise be opaque. CORS Access-Control-Allow-Origin is the opposite conversation: a named origin may read the body. You often want CORP on HTML and private APIs, and CORS only where a browser client on another origin must read JSON. COEP require-corp on a document then requires CORP or CORS on everything it embeds. RoastMyOpsec grades CORS wildcards on app-shaped responses. It does not score CORP today and it does not attempt Spectre-class tests.

### The practical difference

CORP is a lock on being pulled in. CORS is a key for being read. A marketing homepage can set CORP same-origin so random sites cannot no-cors load your HTML as a mysterious subresource. A public widget JS file that others must <script src> cannot use CORP same-origin — that would break the embed. COEP is how an app opts into requiring CORP on its whole tree; brochure sites should not copy that from a lab.

See COOP vs COEP for isolation headers, and Timing-Allow-Origin vs CORS for RUM stopwatches.

### How to choose based on the resource

HTML documents: CORP same-origin is a reasonable default with framing controls. Authenticated JSON: CORP same-origin plus CORS allowlist for the SPA origin only. CDN scripts meant for other sites: no CORP same-origin; use SRI. If you turned on COEP, every cross-origin asset needs CORP or CORS — that is why marketing GTM dies.

| Control | Question it answers | When it wins | Takeaway |
| --- | --- | --- | --- |
| CORP same-origin | May others no-cors load this? | HTML, private APIs, user images | Default lock |
| CORP same-site | Same, but sister hosts OK | www + static. on one eTLD+1 | If you actually share that way |
| CORS allowlist | May this origin read the body? | SPA calling api. | Named origins; see wildcard guide |
| COEP require-corp | Must every embed play along? | App isolation, not brochure GTM | See COOP vs COEP |

### What the roast can see

Wildcard CORS is the finding. Missing CORP is not an automatic F. Confirm CORP at the CDN so HTML is not using the same header pack as a public widget script.

### Common mistakes

The first mistake is CORP same-origin on a script other sites must load.

The second mistake is CORS * plus CORP same-origin on the same API (the messages fight).

The third mistake is COEP on www because a checklist said 'isolation.'

### FAQ

**Is missing CORP a vulnerability?**

It is missing isolation, not an open API. Prioritize CORS wildcards, CSP, and framing. Add CORP on documents and private APIs when you can test embeds.

**Does RoastMyOpsec score CORP?**

Not as a free header gate. CORS wildcards in app mode are scored. Confirm CORP in staging.

**Do I need CORP if I have CORS?**

They answer different questions. CORS is read access. CORP is no-cors embedding. Many APIs want both: tight CORS and CORP same-origin.

---

## composer.json vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/composer-json-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2022-07-24

composer.json vs package.json on production origins is two language manifests that must not sit next to index.html. /package.json donates npm names and versions. /composer.json (and composer.lock) donates PHP packages and constraints — the same inventory class for WordPress plugins, Laravel, and Symfony shops. Neither is a pentest. Both shrink reconnaissance and feed advisory lookups. A private Packagist or npm registry is the public catalog. The marketing CDN is not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /composer.json, does not parse lockfiles on the URL scan, and does not install packages.

### The practical difference

package.json is Node. composer.json is PHP. Dockerfile is how the image is built. composer.lock and package-lock.json pin versions more tightly than the manifest — still do not serve them from the brochure origin. OSV deepen on a repo you own is a different channel than a GET to the CDN.

A 200 on /composer.json is an information leak even when every version is already on Packagist.

### How to choose what to publish

CI: fail if composer.json, composer.lock, package.json, or lockfiles appear in a static marketing artifact. WordPress: the app may live on the same host — still deny those filenames at the CDN for paths that are not the app's private deploy. Keep manifests in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | JS names and versions | Bounded GET | See public package.json vs OSV |
| /composer.json | PHP names and versions | Not scored; deny anyway | Same class |
| /composer.lock | Pinned PHP tree | Not scored | Deny the name |
| /Dockerfile | Image build inventory | Not scored | See Dockerfile vs package.json |

### When composer.json still wins (off the public origin)

It wins in a private repo and in CI. Packagist already lists public packages. The brand origin does not need to mirror the file. Pair with phpinfo vs verbose errors if the same host also dumps the runtime.

### What the roast can prove

A reachable /package.json can surface. composer.json is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a GitHub repo you own.

### Common mistakes

The first mistake is deploying the whole Laravel tree to the document root including composer.json.

The second mistake is treating Packagist visibility as a reason the CDN should serve the file.

The third mistake is Disallow: /composer.json and calling it access control.

### FAQ

**Is a public composer.json a vulnerability?**

It is an information leak. Remove it from the public origin. It is not a credential dump by itself.

**Does RoastMyOpsec fetch composer.json?**

Not as a scored path today. It does probe /package.json. Deny Composer filenames at the edge anyway.

**Does composer.lock belong on the CDN?**

No. Keep lockfiles in the private repo. Use OSV on a repo you own, not a marketing URL.

---

## CircleCI config vs GitHub Workflows on Production Origins

- URL: https://roastmyopsec.com/blog/circleci-config-vs-github-workflows-on-production
- Category: Vulnerabilities
- Updated: 2022-06-29

CircleCI config vs GitHub workflows on a production origin is two CI description trees sitting where only the website should live. Workflow YAML can name jobs, third-party actions, environments, and sometimes plaintext secrets someone pasted 'for a minute.' Serving that next to index.html is the same class of miss as /package.json. RoastMyOpsec may not fetch CircleCI or GitHub workflow files today. Still deny those names at the CDN. Do not list CI paths in robots.txt — that is a map, not a lock.

### The practical difference

CircleCI typically lives as config YAML in a CI folder. GitHub Actions lives as workflow YAML under .github. Different vendors, same leak: the production document root is not your pipeline.

Hardening workflows — least privilege, no long-lived tokens in YAML — belongs in the private repo. Publishing the YAML does not make it safer. It donates the pipeline map.

### How to choose based on what you shipped

Fail CI (ironically) if workflow YAML appears in a static marketing artifact. Deny those tree prefixes at the CDN without advertising them in robots.txt. If a public file may have held a token, rotate it. Keep pipeline files in git, not in dist/.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| GitHub workflow YAML in git | Private repo Actions you control | Do not copy .github into the bucket | Never as a CDN object |
| CircleCI config in git | Private CircleCI projects | Orbs and contexts are still a map if public | Same class of miss on the origin |
| Either YAML on the origin | Never | Pipeline plus possible secrets | Deny the names at the edge |
| /package.json on the origin | Almost never | Inventory cousin | Roast signatures this path today |

### When each CI file wins (off the public origin)

GitHub workflow files win in a private repository with least-privilege tokens. CircleCI config wins in that same private arrangement for CircleCI users. Neither wins as a downloadable object on the brochure host.

A public GitHub repo already publishes workflows by design. That is a product choice. Mirroring those files onto a marketing CDN is still a leftover sync, not a requirement.

### Common mistakes

The first mistake is rsyncing the repo including .github or .circleci because the static site generator lived in the same tree.

The second mistake is pasting a cloud token into YAML 'temporarily' and then shipping that file to the origin.

The third mistake is Disallowing CI folders in robots.txt instead of denying them at the CDN.

### What a URL roast can prove

The roast signatures /package.json, /.env, and /.git/HEAD among a short leftover list. CircleCI config and GitHub workflow paths are not scored today. Still deny them. Bounded GETs, no workflow dispatch, no secret use. Confirm the bucket.

### FAQ

**Is public CI YAML a vulnerability?**

On a marketing origin it is an information leak and, if it held secrets, a credential incident. Remove it and deny the names at the edge.

**Does RoastMyOpsec fetch CircleCI or GitHub workflow files?**

Not as scored paths today. It does probe other well-known leftovers. Deny CI filenames at the CDN anyway. Same class of miss as /package.json.

**Our GitHub repo is public. Why deny workflows on the CDN?**

The repo is one channel. The CDN is another leftover sync that also tends to drag .env and lockfiles with it. Deny the names on the origin either way.

---

## Cargo.toml vs package.json on Production Origins

- URL: https://roastmyopsec.com/blog/cargo-toml-vs-package-json-on-production
- Category: Vulnerabilities
- Updated: 2022-05-26

Cargo.toml vs package.json on production origins is two language manifests that must not sit next to index.html. /package.json donates npm names and versions. /Cargo.toml (and Cargo.lock) donates crate names, features, and pins — the same inventory class for Rust shops that published the crate root. Neither is a pentest. Both shrink reconnaissance and feed advisory lookups. crates.io is the public catalog. The marketing CDN is not. RoastMyOpsec probes /package.json with a bounded GET. It does not currently score /Cargo.toml, does not parse Cargo.lock on the URL scan, and does not fetch crates.

### The practical difference

package.json is Node inventory. Cargo.toml is the crate manifest. Cargo.lock is the exact graph. go.mod is Go. Gemfile is Ruby. Dockerfile is how the image is built. .env is secrets.

A 200 on /Cargo.toml is an information leak even when every crate looks boring. Deny the names at the edge.

### How to choose what to publish

CI: fail if Cargo.toml, Cargo.lock, package.json, or lockfiles appear in a static marketing artifact. App origins that must serve those files are rare — prefer not. CDN: deny those basenames. Keep Rust files in the private repo. Staging hostnames that are guessable get the same deny list.

| File | Typical leak | Roast today | Takeaway |
| --- | --- | --- | --- |
| /package.json | npm names and versions | Bounded GET | See public package.json vs OSV |
| /Cargo.toml | Crate names and features | Not scored; deny anyway | Same class |
| /Cargo.lock | Exact crate graph | Not scored | Deny the name |
| /.env | Live secrets | Bounded signature GET | See .git vs .env |

### When Cargo.toml still wins (off the public origin)

It wins in a private repo and in CI. It never wins as a downloadable object on the marketing bucket. Pair with go.mod vs package.json if the same zip also shipped Go modules.

### What the roast can prove

A reachable /package.json can surface. Cargo.toml is not a scored path. Confirm a 404 yourself. Pair with OSV lockfile deepen on a repo you own — that is a different channel than a URL roast.

### Common mistakes

The first mistake is copying the whole crate to object storage because wasm-pack output lived next to Cargo.toml.

The second mistake is a path or git dependency in a public Cargo.toml that names an internal repo.

The third mistake is Disallow: /Cargo.toml and calling it access control.

### FAQ

**Is a public Cargo.toml a vulnerability?**

It is an information leak. Remove it and deny the name. It is not always a credential dump, but it is not harmless.

**Does RoastMyOpsec fetch Cargo.toml?**

Not as a scored path today. It does probe /package.json. Deny Rust inventory filenames at the edge anyway.

**Does Cargo.lock belong on the CDN?**

No. Keep lockfiles in the private repo. Use OSV on a repo you own, not a marketing URL.

---

## backup.sql.gz vs dump.sql on Production Origins

- URL: https://roastmyopsec.com/blog/backup-sql-gz-vs-dump-sql
- Category: Vulnerabilities
- Updated: 2022-04-13

backup.sql.gz vs dump.sql on a production origin is the same database souvenir in two costumes. /dump.sql is often schema and rows in text — the roast signatures SQL-shaped bodies on that well-known name. A .sql.gz is the compressed twin: still a dump, still not a marketing asset. Compression is not access control. RoastMyOpsec may not fetch backup.sql.gz today. It does probe /dump.sql and /backup.zip. Still deny dump and archive names at the edge. Do not Disallow them in robots.txt; that publishes a map of leftovers.

### The practical difference

Plain dump.sql is easy to recognize. gzip only changes the wrapper. Either file on a public bucket is a data exposure until proven empty of live rows and credentials.

backup.zip is a cousin the roast already signatures by magic bytes. Directory listing of a backup folder is louder. None of these belong next to index.html. Put archives in private storage with no public ACL.

### How to choose based on what you shipped

If any dump-shaped object was public: take it off the origin, deny dump and archive patterns at the CDN, rotate database and app secrets that may have been in the file, and stop writing backups into the document root. CI should fail if those extensions appear in the publish artifact.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| /dump.sql | Never on the CDN | Schema and row data | Roast signatures this name today |
| backup.sql.gz / dump.sql.gz | Never on the CDN | Same dump, smaller file | Not always fetched today; deny anyway |
| /backup.zip | Never on the CDN | May hold the dump plus .env | Roast signatures zip magic |
| Private object storage | Retention you actually need | Public ACL recreates the miss | Not the website origin |

### When a dump still wins (never as a public URL)

A SQL dump wins in private backup storage with encryption and tight IAM. gzip wins there to save bytes. Neither wins as a guessable path on the brochure host.

Turning off directory listing is necessary and not sufficient. The exact URL can still 200. See directory listing vs 403.

### Common mistakes

The first mistake is 'it is gzipped so nobody will read it' on a public URL.

The second mistake is dumping to the web host because disk was handy, then forgetting the file.

The third mistake is Disallowing /backup in robots.txt as if crawlers were the threat.

### What a URL roast can prove

Bounded GETs to /dump.sql and /backup.zip with content signatures. .sql.gz names are not a guaranteed scored path today. A clean roast is not a guarantee a uniquely named archive is absent. No unzip-as-exploit, no query against your database.

### FAQ

**Is a public .sql.gz a vulnerability?**

Yes — it is a data exposure. Remove it, deny archive names at the edge, and rotate secrets that may have been inside.

**Does RoastMyOpsec fetch backup.sql.gz?**

Not as a dedicated scored name today. It does probe /dump.sql and /backup.zip. Deny compressed dumps at the CDN anyway. Same class of miss.

**If listing is off, is the gzip safe?**

No. Guessable names still download. Keep dumps off the public origin.

---

## AWS Credentials File vs .env on Production Origins

- URL: https://roastmyopsec.com/blog/aws-credentials-file-vs-env-on-production
- Category: Vulnerabilities
- Updated: 2022-04-05

An AWS credentials file vs .env on a production origin is two containers for the same class of secret. /.env often holds AWS_ACCESS_KEY_ID-shaped values — the roast signatures that path. A copied credentials file from a developer home directory is the shared-file version of the same miss. Neither belongs next to index.html. RoastMyOpsec may not fetch that credentials filename today. Still deny it at the edge. Do not Disallow it in robots.txt; deny the object, do not advertise the path.

### The practical difference

The credentials file is the CLI/SDK shared-config convention. .env is the twelve-factor convention the app loads. Attackers do not care which format you used once the bytes are public.

IAM best practice is temporary roles, not long-lived keys in any file on a CDN. If a public object may have held access keys, rotate them. Then fix the publish pipeline so home-directory files never enter the artifact.

### How to choose based on what you shipped

Take the object off the origin, deny the basename at the CDN, rotate access keys that may have been in that tree, and prefer workload roles going forward. Fail CI if credential-file names appear in a static export. Same deny on guessable staging hosts.

| Option | When it wins | Watch-out | Takeaway |
| --- | --- | --- | --- |
| .env with AWS keys on the origin | Never | Live secrets | Incident; roast signatures /.env |
| Credentials file on the origin | Never | Same keys, different filename | Same class of miss; deny the name |
| IAM role / temporary creds | Compute and CI you control | Do not dump role output into static files | No long-lived keys on the CDN |
| robots.txt Disallow | Not for this | Treasure-map line | Edge deny is the control |

### When each option wins (never as a public download)

.env wins only as a private runtime file on the app host — and even then, prefer platform secrets or roles. A local credentials file wins on a locked-down workstation for CLI work. Neither wins on the marketing bucket.

If /.git/HEAD was also reachable, assume history as well as the current file. See exposed .git vs .env.

### Common mistakes

The first mistake is zipping a developer home folder into the deploy because a script 'needed AWS for the build.'

The second mistake is rotating the .env keys and leaving the credentials file on the origin.

The third mistake is documenting the filename in robots.txt Disallow instead of blocking it at the CDN.

### What a URL roast can prove

The roast signatures /.env and /.env.local. An AWS credentials filename is not a scored path today. Still deny it. Bounded GETs, signature checks, no AWS API calls, no key use. A 404 is not proof the file is gone under another prefix.

### FAQ

**Is a public AWS credentials file a vulnerability?**

Treat it as a credential incident. Remove it, deny the name at the edge, and rotate keys that may have been inside.

**Does RoastMyOpsec fetch AWS credentials files?**

Not as a scored path today. It does probe /.env. Deny the credentials filename anyway. Same class of miss as /.env.

**Are publishable AWS keys in JavaScript the same issue?**

Browser keys are a different placement rule. Long-lived secret keys must never ship. See public JavaScript API keys vs server secrets.

---

## App Auth Paths vs Website Login Pages

- URL: https://roastmyopsec.com/blog/app-auth-paths-vs-website-login
- Category: Vulnerabilities
- Updated: 2022-03-10

App auth paths vs website login pages is a surface split. A marketing /login or CMS /wp-login.php is a document. App origins expose /api, /oauth, /auth, /graphql, and /v1 — discoverability plus 401/403 behavior. Presence is normal. 401/403 is healthy. 200 on an authenticated JSON API without credentials is the miss. RoastMyOpsec app mode uses gentle path probes for status codes. No credential stuffing. Website mode still belongs on the brochure host.

### The practical difference

Website login is often a form and cookies. App auth is tokens, OAuth redirects, and JSON. They fail differently: the site misses MFA on WordPress; the app misses rate limits on /oauth/token or leaves GraphQL wide open.

Many products put the costume on www and the sessions on app. Scanning only www is how API doors stay lonely.

### How to choose what to scan

Run website mode on the marketing hostname. Run app mode on the origin that serves APIs. If one host does both, run website first for headers, then app for CORS, methods, errors, and auth paths.

| Path class | Better mode | Healthy signal | Takeaway |
| --- | --- | --- | --- |
| /login, /wp-admin | Website (also see admin guide) | MFA + rate limits | Discoverability ≠ breach |
| /api, /v1 | App | 401/403 without a body novel | Pair with verbose-errors guide |
| /oauth, /auth | App | Rate limits, bot fights, MFA | This is where stuffing lands |
| /graphql | App | No public schema dump | See introspection guide |

### When 200 on /api is still fine

A public health endpoint or unauthenticated read API can 200 on purpose. The roast is about whether that body should be public and whether errors still leak internals. Do not confuse a documented public API with an accidentally open admin router.

### Common mistakes

The first mistake is hardening www and leaving api. on the origin IP.

The second mistake is rate-limiting /login HTML and not /oauth/token.

The third mistake is treating 401 as a failure instead of a healthy lock.

### FAQ

**Is a public /api path a vulnerability?**

No. APIs are supposed to be reachable. The question is authorization, abuse controls, and error hygiene on those paths.

**Should I hide /oauth with robots.txt?**

No. That advertises it and does not authenticate anyone. See robots vs access control.

**How does RoastMyOpsec probe auth paths?**

Soft GETs for presence and status. No password spraying, no credential stuffing.

---

## Admin Login Discoverability vs Access Control

- URL: https://roastmyopsec.com/blog/admin-login-discoverability-vs-access-control
- Category: Vulnerabilities
- Updated: 2022-01-27

Admin login discoverability vs access control is a mismatch that shows up on marketing sites and SaaS origins alike. Finding /admin, /wp-admin, or /login is reconnaissance, not a breach. Guessable URLs deserve MFA, SSO, rate limits, and monitoring — not a clever rename and a Disallow line. RoastMyOpsec's vault check uses soft probes for status codes only. No password spraying, no brute force, no exploit payloads.

### The practical difference

Discoverability answers 'does a common path exist?' Access control answers 'can a stranger use it?' A 200 login form on /wp-admin is normal for WordPress. A 200 admin JSON API with no auth is not.

Moving the panel to /secret-dashboard still leaves login as the hard problem. Obscurity fails the moment someone shares a screenshot or robots.txt lists the path.

### How to choose what to do first

Protect the identity plane: SSO or MFA on every admin user, lockouts, and alerts on failures. Then decide whether the panel should even be on the public internet (VPN or IP allowlist for staff tools).

| Finding | Means | First move | Takeaway |
| --- | --- | --- | --- |
| 200 on /login | A form exists | MFA, rate limits, boring errors | Presence is not a CVE |
| 200 on /wp-admin | WordPress admin is reachable | SSO/MFA plugins, hide is optional | Still patch WordPress |
| 401 / 403 on /admin | Control is answering | Keep it; add monitoring | Healthy evidence |
| Disallow /admin in robots | You published the map | Auth, then boring robots | See the robots vs access-control guide |

### When hiding the path still fails

Default CMS paths are in every scanner's wordlist. A unique path only slows script kiddies who never guess; it does not replace credentials.

Preview deployments that copy prod admin without SSO are how 'we locked production' still leaks.

### Common mistakes

The first mistake is renaming /admin and calling the ticket done.

The second mistake is MFA on the human login and a basic-auth bypass on /xmlrpc.php or a leftover /phpmyadmin.

The third mistake is rate-limiting /login and leaving GraphQL mutations unbounded.

### FAQ

**Is a public /admin page a vulnerability?**

Not by itself. It is attack surface. The vulnerability is weak or missing authentication, no MFA, or no abuse controls on that surface.

**Should I hide wp-admin?**

You can, but you still need updates, MFA, and rate limits. Hiding is optional seasoning, not the meal.

**How does RoastMyOpsec probe admin paths?**

Soft GETs for status codes on common paths. No password spraying, no brute force.

---

## Access-Control-Expose-Headers vs CORS Allow-Origin

- URL: https://roastmyopsec.com/blog/access-control-expose-headers-vs-cors
- Category: Vulnerabilities
- Updated: 2022-01-10

Access-Control-Expose-Headers vs CORS Allow-Origin is two Fetch CORS knobs. Access-Control-Allow-Origin decides which browser origins may read the response at all. Access-Control-Expose-Headers names extra response headers that script may read besides the CORS-safelisted ones (Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma). Exposing Authorization, Set-Cookie, or a debug request-id to * callers is how you donate metadata. Timing-Allow-Origin is yet another header for Resource Timing, not ACEH. RoastMyOpsec app mode sends a bounded untrusted Origin and reads ACAO and credentials. It does not score Access-Control-Expose-Headers, does not read custom headers as a user, and does not attempt to skip CORS.

### The practical difference

ACAO is 'who.' ACEH is 'which headers that who may inspect.' A public weather API can use ACAO * and a short ACEH list. A cookie API cannot use ACAO * at all — see credentials vs wildcard. Max-Age only caches preflight; it does not expose headers.

Safelisted headers are already readable after a successful CORS check. You do not need to expose Content-Type. You do need ACEH for X-Request-Id if the SPA reads it — and you should ask whether that ID belongs in JS.

### How to choose what to expose

Expose only headers the first-party SPA must read. Prefer a named ACAO. Do not expose Set-Cookie (browsers will not give JS cookies anyway if HttpOnly). Do not expose WWW-Authenticate details. Do not copy ACEH * from a gateway default onto an authenticated API.

| Header | Job | JS can read after CORS? | Takeaway |
| --- | --- | --- | --- |
| Access-Control-Allow-Origin | Which origin may read the response | N/A — it is the gate | Allowlist; see wildcard guide |
| Access-Control-Expose-Headers | Which extra headers JS may read | Only names you list (plus safelist) | Minimize; no debug dumps |
| Timing-Allow-Origin | Resource Timing visibility | Different API | See TAO vs CORS |
| Access-Control-Max-Age | Preflight cache TTL | No | See Max-Age vs preflight |

### What the roast can prove

ACAO reflecting an untrusted Origin can surface. A generous ACEH list is not scored today. Hunt Expose-Headers in your gateway. Pair with verbose API errors if the exposed header is a stack trace id.

### Common mistakes

The first mistake is Access-Control-Expose-Headers: * on a user API.

The second mistake is exposing a header that contains a signed URL.

The third mistake is tightening ACAO and leaving ACEH copied from a public CDN preset.

### FAQ

**Does Expose-Headers let other sites call my API?**

No. Allow-Origin (and credentials) decide who may read. ACEH only names extra headers after that gate passes.

**Should I expose X-Request-Id?**

Only if the SPA must show it. Prefer logging it server-side. Do not expose it to every origin.

**Does RoastMyOpsec score Access-Control-Expose-Headers?**

No. It reads ACAO and credentials from a bounded Origin.

