Permissions Policy, Browser Capabilities, and Privacy
A standards-led guide to Permissions Policy, browser capability checks, and privacy-aware embedded feature tests.
BotBrowser Team
Want the structured docs for Platform?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
BotBrowser can repeat an authorized browser journey with a declared context and observe a visible result. It cannot author or deploy a site's policy, grant permission, create a device, or prove a remote business record. Permissions Policy limits which documents may use selected browser capabilities. The response from the top-level document sets an upper bound; an iframe allow attribute can delegate a named feature to a child origin. This is a capability boundary, not a user permission grant, a device guarantee, or proof that an application operation completed.
For camera, microphone, geolocation, fullscreen, and similar features, record the delivered policy, final origin, browser implementation, permission decision, and visible application result as separate observations. This article follows the W3C Permissions Policy specification and MDN's reference.
TL;DR
Use the narrowest feature and origin allowlist. Check the final origin after redirects, keep allow aligned with the response header, and test an owned control/candidate fixture. A policy-eligible frame may still lack an API, secure context, user activation, permission, device, or application acknowledgement. BotBrowser can repeat an authorized browser journey and observe its visible result; it cannot author or deploy a site's policy, grant permission, create a device, or prove a remote business record.
Contents
- What the policy controls
- Separate capability gates
- Keep policy boundaries distinct
- Use a reproducible fixture
- Practical conclusion
What the policy controls
An example response, Permissions-Policy: geolocation=(self "https://widget.example"), makes the top-level origin and the named widget eligible to attempt geolocation. The iframe still needs a matching allow="geolocation", and every ancestor must preserve the delegation. A redirect to another scheme, host, or port can make the final child ineligible. Feature defaults differ, so consult the current feature definition rather than assuming every capability starts enabled or disabled.
Do not use allow="*" as a convenience. It delegates the named feature to any origin that occupies the container, including an unexpected redirect or nested document. Keep an explicit allowlist, review who owns the response header and embed markup, and expire entries when an integration ends. A child cannot grant itself a feature removed by an ancestor response.
Policy eligibility is not permission state. navigator.permissions may be prompt or granted while the frame is blocked by policy. A policy-eligible document may still receive a denial, timeout, unavailable-device error, or browser-specific rejection. Report the first gate that failed instead of collapsing every outcome into “policy failed.”
Separate capability gates
| Gate | Evidence to capture | What a pass does not prove |
|---|---|---|
| Final origin and context | Final URL, scheme, port, secure-context state | The API is implemented |
| API capability | A narrow check such as typeof navigator.geolocation.getCurrentPosition | A request will be allowed |
| Permissions Policy | Response header, allow, ancestor chain, child origin | User consent or a device |
| User/platform decision | Permission result, administrator setting, device status | Application data was accepted |
| Application result | Visible status or owned acknowledgement within a timeout | A remote record was committed |
The same separation applies to nested frames. Ask the child to report its own final origin and status through an owned test page; do not infer it from the parent's state. Keep the browser's permission state synthetic and stable for repeatable tests, and do not collect real coordinates, media, credentials, or account content.
Keep policy boundaries distinct
Content Security Policy (CSP) controls which scripts, connections, resources, and frames may load. Cross-Origin-Opener-Policy (COOP) shapes relationships between top-level browsing contexts. Cross-Origin-Embedder-Policy (COEP) controls conditions for embedding cross-origin resources. None of these headers delegates geolocation or camera access, and Permissions Policy does not create cross-origin isolation or satisfy CORS/CORP.
Check network reachability and CSP first, then final origin and any required COOP/COEP state, then the Permissions Policy chain, and only then request a user-mediated feature. A network error, policy block, user denial, and application error have different owners and different fixes. None of these browser mechanisms replaces server authorization, consent UX, input validation, or device controls.
Use a reproducible fixture
Serve two owned pages at stable routes such as /fixtures/permissions-policy/control.html and /fixtures/permissions-policy/candidate.html. Keep the frame origin, button, status marker, and route identical. The candidate receives the proposed header; the control intentionally omits that delegation. The child reports policy=blocked or policy=allowed only after an explicit click. These strings are application markers, not browser evidence by themselves; also assert the delivered Permissions-Policy, iframe allow, and final child origin.
import { test, expect } from '@playwright/test';
const cases = [
{ name: 'control', url: 'https://qa.example.test/fixtures/permissions-policy/control.html', expected: 'blocked' },
{ name: 'candidate', url: 'https://qa.example.test/fixtures/permissions-policy/candidate.html', expected: 'allowed' },
];
for (const scenario of cases) {
test(`Permissions Policy ${scenario.name}`, async ({ page }) => {
let response;
try {
response = await page.goto(scenario.url, { waitUntil: 'domcontentloaded', timeout: 8000 });
} catch (error) {
throw new Error(`NETWORK_ERROR before policy assertion: ${error.message}`);
}
expect(response?.ok(), `HTTP response for ${scenario.name}`).toBeTruthy();
await page.getByRole('button', { name: 'Request location' }).click();
await expect(page.getByTestId('policy-result')).toHaveText(new RegExp(`^${scenario.expected}$`), { timeout: 5000 });
});
}
Keep navigation and status waits bounded. DNS, certificate, HTTP, and missing-route errors remain NETWORK_ERROR; they must not be converted into a passing blocked result. A timeout means the fixture produced no evidence within its window, not that the browser denied the feature. On teardown, close the context and discard temporary data. A synthetic fixture is evidence for its declared browser and route, not a universal compatibility or privacy claim.
Practical conclusion
Write an embed contract naming the top-level origin, final child origin, feature, purpose, user action, fallback, and owner. Re-run the fixture after a vendor host, redirect, browser release, or nested frame changes. Keep reports to policy values, origins, browser release, status markers, and visible outcomes.
BotBrowser can hold a declared browser context steady and repeat an authorized control/candidate journey. It cannot change a site's delivered Permissions-Policy, alter an iframe origin, grant user permission, provide a camera or microphone, override CSP/COOP/COEP, repair a vendor response, or prove that a remote business operation committed. The delivered response and the application's server records remain authoritative. For adjacent context, see the CSP guide and the cross-origin isolation guide.
A review worksheet for teams.
Start the review with a one-line capability claim. For example: “The checkout frame may request camera access after the user selects document capture.” This is more useful than “the checkout is trusted” because it identifies the feature, the document, and the user action. Add the top-level origin, the expected child origin after redirects, the scheme and port, and the route that serves the response. If any value is dynamic, document how it is resolved and which owner approves a change.
Next, write the smallest policy expression that supports the journey. List one feature per row and state why the feature is required. A document that needs fullscreen does not automatically need camera, microphone, geolocation, or payment. A widget that needs microphone access for a support call does not need the microphone on every page of the product. Removing unused entries makes a later incident easier to explain and reduces the number of browser prompts a user can encounter.
Record the relationship between the response header and the container declaration. The header is an upper bound that an ancestor controls; allow is a delegation at the embedding boundary. Both must describe the same feature and compatible origins. In a nested integration, repeat the check for every ancestor. A frame can load successfully while a later ancestor prevents the feature from being used. Keep a copy of the final response observed through the production route, because a configuration file or source template is not proof of what a CDN or service worker delivered.
The worksheet should include a “not proven” column. A policy pass does not prove the API exists. An API check does not prove a user will consent. A granted permission does not prove a device is connected or that the application accepted its output. A visible success message does not prove a server wrote a durable record. These distinctions are modest but important: they prevent a compatibility receipt from being reused as a privacy, authorization, or compliance claim.
Privacy-aware feature handling.
Ask for a feature only when the user understands its purpose. A camera control should explain whether the image is processed locally, uploaded, or discarded, and should provide a keyboard-accessible cancel path. A microphone control should show when capture is active and stop tracks when the task ends. A geolocation flow should offer a manual alternative and should not silently retry after a timeout. A fullscreen flow should preserve readable content when the browser rejects the transition. Permissions Policy cannot make these choices for the product.
Keep test data synthetic. A geolocation fixture can return a fixed coordinate that is clearly not a person's location. A media fixture can use an approved test provider or a generated stream. A candidate page should not send the resulting value to an analytics service merely to prove that the callback ran. The useful receipt is the policy state and the bounded visible outcome. If a service-side assertion is required, record it in the service test with its own owner and retention rules rather than adding account data to a browser log.
Be precise about privacy language. A narrow origin allowlist reduces an accidental delegation risk; it does not make a frame anonymous. Separate browser contexts can prevent state carryover between authorized test attempts; they do not prevent a service from correlating requests that it controls. A policy header can restrict a document's feature use; it does not hide network metadata, remove server logs, or establish legal compliance. Use “limits,” “allows,” “observed,” and “not evaluated” instead of “protects everything” or “guarantees privacy.”
Failure classification and ownership.
When the candidate does not produce the expected result, preserve the first observable failure. A DNS or certificate problem belongs to availability or infrastructure. A response missing the intended header belongs to deployment configuration. A frame that loads but reports blocked belongs to the policy chain or a feature-specific browser condition. A frame that reports allowed but receives a user denial belongs to consent and browser state. A successful API callback followed by a failed save belongs to the application or service boundary. This classification gives the incident to the right owner without changing the policy to make a test green.
For each failure, retain the final URL, response status, relevant policy value, iframe allow value, child status marker, browser release, and test timestamp. Do not retain cookies, authorization headers, raw media, coordinates, or a full account trace. Console output can support triage, but it is not a substitute for the controlled status marker. If the child never ran, report child-not-reached; do not guess whether the cause was a missing token, CSP, or a network failure.
Run negative cases as part of the fixture. Remove the feature from the candidate response and verify that the child reports a policy result while the rest of the page remains usable. Change the child to an unlisted origin and verify that the result changes without granting a wildcard. Add a nested frame that omits its own delegation. Redirect the child to a different port or scheme. These cases exercise the allowlist and ancestor rules without requesting a real device or personal value.
Change review and rollback.
Review the contract whenever a vendor changes its host, a CDN introduces a redirect, a browser release changes a default allowlist, or a product adds a nested frame. The contract should identify the expected final origin rather than only the URL typed into markup. Keep the previous response configuration, frame markup, and application version available for rollback. If an essential widget fails, restore the known-good pair and rerun the same control/candidate fixture before considering an exception.
An emergency exception should be narrow and expiring. Name the feature, origin, owner, reason, expiry date, and follow-up fixture. Do not solve a rollout problem by adding *, deleting the entire header, or silently granting a feature to every tenant. A temporary exception that cannot be explained later is an operational risk. A rollback that changes policy and markup together is easier to verify than one that leaves half of the old contract in place.
Reproducibility notes.
Keep the route, locale, permission state, browser release, and assertion stable when comparing two policy revisions. Change one variable at a time. If the visible result differs, record the difference before proposing a cause. A browser update can change an implementation gate while the response header stays identical; a CDN change can alter the delivered header while the application source stays identical. The fixture should make that distinction visible.
Bounded waits are part of the contract. A navigation timeout is a missing availability receipt, not a policy denial. A status timeout is missing fixture evidence, not an automatic refusal. Fail with the original error and include diagnostics separately. Close the context after each case so a permission decision or storage value cannot leak into the next case. If a case needs a real account or device, move it to an authorized service or hardware test and link its owner; do not broaden this browser fixture.
The result of a good review is a small, durable record: the feature and purpose, the top-level and final child origins, the delivered policy and allow, the browser and permission preconditions, the visible result, the fallback, and the owner of any unproven claim. This record is enough to explain a regression and to rerun the same question after a browser or vendor change. It does not need a profile dump, a user trace, or a broad inventory of browser signals.
Teams can make the record easier to audit by assigning a stable fixture identifier and a review date. The identifier should refer to synthetic test data, not to a customer or a persistent browser profile. Store the expected policy and the observed policy beside the result, and note whether a redirect, managed browser setting, or feature default affected the run. A reviewer should be able to tell which observation came from the browser and which statement came from an application or service owner.
The same discipline helps when several widgets share a page. Give each widget its own origin row and feature purpose. Do not copy an allowlist from one vendor into another integration because the names look similar. A payment frame, a support frame, and a document-capture frame can have different owners and different user journeys even when they all use an iframe. Keep their fallback controls independent so one unavailable capability does not disable the rest of the page.
Finally, publish only the claim that the evidence supports. A public compatibility note can say that a declared browser and route produced policy=allowed under a synthetic permission state. It should not say that every browser will expose the feature, that a user will consent, or that the service has retained no data. Those stronger statements require separate product, infrastructure, and governance evidence. Clear scope makes the article useful to implementers without turning a narrow browser observation into a privacy promise.
Sources
- W3C: Permissions Policy
- MDN: Permissions Policy
- MDN: iframe
allow - MDN: CSP
- MDN: COOP
- MDN: COEP
- BotBrowser: Advanced features
BotBrowser Team
Related Articles
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.