Add Security Headers to a Static Resource Directory Without Breaking It

A static resource directory can gain meaningful browser protections from one _headers file, but the safest rollout is incremental. Begin with low-risk headers, inventory every script and asset the page actually uses, add a narrowly scoped content policy, deploy to a Preview environment, and inspect real response headers before promoting the change. Do not paste a restrictive policy into production without testing every page state.

This guide addresses one concrete problem: reducing avoidable browser-side risk on a public directory hosted with Cloudflare Pages while keeping its external resource links usable. It assumes the site is composed of static HTML, CSS, JavaScript, fonts, and images. Responses produced by Pages Functions require a different implementation in the function code.

Secure Gateway - an overview | ScienceDirect Topics

Understand what the file controls

Create a plain-text file named _headers with no extension. It must be included in the final directory that Cloudflare Pages deploys. If a framework copies files from public or static into a build directory, place the source file where that copy process will preserve it. For a hand-built static site, put it directly in the uploaded output folder.

The file contains path patterns followed by indented header lines:

/*
 Header-Name: value
 Another-Header: value

The first line matches every static path. More specific blocks can adjust selected areas. When multiple rules match, their headers can combine, so maintain one clear owner for each policy instead of repeating the same header across several blocks.

This mechanism applies to static asset responses. A request handled by a Pages Function does not automatically inherit these rules. If the project later adds server-generated routes, attach the required headers to those responses separately and test them as a different delivery path.

Inventory the page before restricting it

A content policy describes which sources a browser may load, but that policy cannot be correct until you know what the site actually loads. Before writing rules, inspect the rendered page and catalog every single dependency. Check whether scripts are third-party or inline, whether styles rely on inline blocks or attributes, and whether images are hosted locally, fetched remotely, or generated as data URIs. Confirm whether fonts are self-hosted or system-based, whether scripts make background network requests after loading, whether the directory needs framing by external portals, and whether any forms submit data. For a minimal static directory, baseline assumptions should lean heavily toward local static assets with no background network calls or form submissions.

Use browser developer tools with the cache disabled and exercise every state: initial page, mobile navigation, filter controls, empty results, error page, and any theme switch. A policy that works on the home page can still break a secondary page or icon.

External anchors are navigation, not page subresources. A policy that permits only local scripts and images does not automatically prevent a visitor from following an approved outbound link. Test that behavior explicitly rather than weakening script or image rules unnecessarily.

Start with low-risk response headers

The first release should add protections that rarely break a conventional static directory:

/*
 X-Content-Type-Options: nosniff
 Referrer-Policy: strict-origin-when-cross-origin
 Permissions-Policy: camera=(), microphone=(), geolocation=()

X-Content-Type-Options tells browsers not to reinterpret a resource as a different content type. This works best when the site already serves correct types for scripts, styles, images, and fonts.

Referrer-Policy limits how much address information is sent when a visitor navigates away. The example keeps the origin available for same-security-level cross-origin navigation while withholding the full path. If the directory has a stronger privacy requirement, test no-referrer as a deliberate alternative.

Permissions-Policy disables browser capabilities the directory does not use. Keep the list tied to the actual site. Adding dozens of unfamiliar directives creates noise without replacing an application review.

Deploy this small block first. Check normal pages, the not-found page, CSS, JavaScript, images, and downloadable files before adding a more restrictive policy.

Prevent unwanted framing

If the directory is not designed to appear inside another site’s frame, declare that boundary in the content policy with frame-ancestors. A compatible legacy header can be retained as defense in depth.

/*
 X-Frame-Options: DENY
 Content-Security-Policy: frame-ancestors 'none';

Do not use this setting if a trusted portal must embed the directory. In that case, document the required embedding origins and use a carefully scoped frame-ancestors rule. Test the actual embedding workflow in Preview before release.

Avoid maintaining different framing decisions in multiple matching blocks. If one block says DENY while another policy permits an origin, reviewers and browsers may receive a confusing combination.

Build a policy from observed needs

For a simple directory with only local assets and no submissions or network calls, a compact policy can be strict without being complicated:

/*
 Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'

Each directive has a specific purpose. The default-src 'self' directive provides a local fallback for unlisted resource types. script-src 'self' allows local script files and rejects inline script by default, while style-src 'self' expects styles to live in local style sheets. img-src 'self' data: allows local images and embedded data images when needed, and font-src 'self' limits fonts to the site’s own origin. connect-src 'none' blocks background network connections from page scripts, object-src 'none' disables legacy embedded objects, and base-uri 'none' prevents a base element from changing relative address resolution. Finally, form-action 'none' is appropriate when the directory has no submitting forms, and frame-ancestors 'none' prevents other pages from framing the site.

Remove permissions the site does not need. For example, omit data: from the image rule when there are no embedded images. If the page fetches a local JSON index, change connect-src to 'self' rather than allowing every destination.

Do not add broad inline allowances merely to silence console errors. Move inline scripts into local files. Move style attributes into named classes. Those changes make the policy clearer and reduce the amount of code that the browser must trust.

Keep outbound references separate from dependencies

A resource directory may link to external collections while loading all of its own assets locally. Preserve this distinction in both code and review notes. An outbound link should not cause its destination to be added to script-src, img-src, or connect-src.

For a manual navigation check, include one reviewed example such as 주소모음 주소온길. Confirm that the link opens only after a user action, carries descriptive anchor text, and does not imply that every destination inside the referenced collection has been independently verified.

If links open in a new tab, use a safe relationship attribute and make the behavior understandable to users. Do not open unfamiliar destinations automatically, preload them, or request their icons from the visitor’s browser. Those behaviors disclose activity and expand the policy for little practical benefit.

Add cache rules only for fingerprinted assets

Security headers and cache headers share the same file, but they solve different problems. Long-lived caching is safe only when a file name changes whenever its contents change.

/assets/app.*.js
 Cache-Control: public, max-age=31536000, immutable
/assets/site.*.css
 Cache-Control: public, max-age=31536000, immutable

Use these rules only if the build produces names containing content-derived hashes. Do not apply immutable caching to index.html, a stable app.js, or a data file whose name never changes. Otherwise, visitors may keep an old directory after a successful deployment.

Keep cache policy in its own documented blocks. During review, confirm that a changed source produces a changed asset name and that the generated HTML points to it.

Test the deployed response, not just the file

A correct-looking _headers file is not proof that the response contains the intended headers. The build might omit the file, a path rule might not match, or a route might be handled by a function.

After deploying to Preview, inspect several real responses:

curl -I "$PREVIEW_PAGE"
curl -I "$PREVIEW_NOT_FOUND_PAGE"
curl -I "$PREVIEW_SCRIPT"

Check the output for each required header and confirm that the content policy appears once with the expected value. Then load the site in a fresh browser profile and review the console. Exercise navigation, filters, images, error handling, keyboard operation, and the approved outbound link.

Verification should follow a systematic check across key interactions. Confirm that the home page response delivers baseline headers and the content policy, while static script responses return the correct content type and nosniff behavior. Ensure the not-found page renders styled properly with headers present. Verify that inline script execution probes are blocked due to the absence of inline allowances, and check that filter controls work locally without attempting remote connections. Finally, test that external references open only after deliberate user actions, framing attempts fail as the page refuses to render inside an iframe, and updated hashed assets load under their new names without retaining outdated HTML.

Do not treat a clean console as the only pass condition. Verify response headers directly and visually confirm important interactions.

Use Preview as a promotion gate

Apply the change to a Preview deployment first. Record the deployment identifier, tested pages, browser versions, policy value, and result. A second reviewer should compare the policy with the dependency inventory.

Promote the exact tested build to production. After promotion, repeat a smaller signed-out check on the public pages.dev address. If the page breaks, roll back the deployment rather than weakening the policy blindly. The browser console should guide a targeted correction, followed by another Preview cycle.

When using Direct Upload, remember that the uploaded directory is the release artifact. Confirm that _headers sits at its top level alongside the entry page. When using a framework, inspect the final build directory rather than assuming the source copy operation succeeded.

Release checklist

Before launching, confirm that the dependency inventory thoroughly covers every page state and that the _headers file is present in the final deployment directory. Ensure low-risk headers were fully tested prior to applying the content security policy, and verify that permissions are strictly limited to capabilities the directory actually uses. All inline scripts and styles should be moved to local files, ensuring outbound links are never added as trusted subresource sources. Confirm that framing decisions match real embedding requirements, long cache lifetimes apply exclusively to fingerprinted assets, and Preview responses were verified directly. Finally, make sure any Pages Functions attach their own required headers independently, promote the exact tested artifact to production, and verify the live public page while signed out.