Browser Platform

Obfuscation and Permissions-Policy

Permissions-Policy is the browser control that decides which powerful features a document is allowed to use at all. It has almost nothing to do with obfuscation, which is exactly why it is worth an article: it is a control that keeps working when your code is fully in the attacker's hands, and it produces a failure that looks convincingly like the transform broke something.

What the header actually controls

Permissions-Policy, which older material calls Feature-Policy, is a response header that lists which of the browser's powerful features a document may use and which origins may use them. Camera, microphone, geolocation, payment, fullscreen, screen capture and a long tail of others are all gated this way. A document that has been denied a feature cannot use it, regardless of what its JavaScript asks for.

It is worth separating it from the headers it gets confused with. Content-Security-Policy governs what code and content may load and execute. Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy govern isolation between browsing contexts. Permissions-Policy governs capabilities: not whether your script runs, but whether the API it calls is available to it. The three are complementary and answer different questions.

The second half of the feature is delegation. An embedded document inherits a policy from its embedder, and the host page can hand a capability down to an iframe with the allow attribute. That is the part that matters commercially, because it means a third-party widget's access to a capability is a decision made by the page that embeds it, not by the widget.

Why a header is worth more than a check in your bundle

This site returns repeatedly to a single idea: a decision that runs on the attacker's machine is a decision the attacker can change. A licence check compiled into your bundle can be found and removed. An expiry date compared against the local clock can be beaten by moving the clock. The reason those pages exist is that the client is not a trustworthy place to enforce anything.

Permissions-Policy sits on the other side of that line, and cleanly. The header is sent by your server and applied by the browser before your code runs. Nothing in the bundle can rewrite it, because the policy is not data your program owns. An attacker who has fully deobfuscated your JavaScript, understands every line and can edit it at will still cannot grant the document a capability the browser was told to withhold. That is a genuinely different class of control from anything a build step provides.

The practical reading is not that the header protects your code, because it does not and it is not trying to. It is that when you are deciding where to spend effort, controls that live outside the bundle keep their value under exactly the conditions where in-bundle controls lose theirs. Obfuscation raises the cost of understanding your code. Headers constrain what that understanding is worth.

The failure it causes, and why the obfuscator gets blamed

Here is the sequence that produces a support ticket. A feature is denied by policy, either because the header changed, because a new deployment target sends a different one, or because your script moved into an iframe that was never granted the capability. The gated call then fails in whichever way that API fails: a rejected promise, a thrown error, a permission query that resolves to denied, or a media request that never yields a track.

In an unprotected build, the stack trace names your function and the console carries a message from the browser saying the feature was blocked by policy. In a protected build the stack names something short and meaningless, the surrounding code is unfamiliar, and the failure arrives during a release that also changed the protection settings. Every visible signal points at the transform, and none of them are the cause.

Two checks separate the cases in about a minute. Reproduce the same page unprotected and see whether the feature still fails, which it will. Then look at the response headers for the document and for any iframe in the chain, because the policy that applies is the one on the document that runs the code, narrowed by any allow attribute above it. If the feature fails unprotected too, protection is not involved and no amount of reconfiguring the transform will help.

The one real interaction: your console is the evidence

There is a genuine interaction between protection and this header, and it is not a compatibility problem. It is a diagnostic one.

When a policy blocks a feature, the browser usually says so in the console, and that message is frequently the only thing that names the policy as the cause. Console suppression, which is a runtime option rather than part of the code transform, replaces thirteen console methods with empty functions: log, info, warn, error, debug, trace, dir, table, group, groupCollapsed, groupEnd, time and timeEnd. Enable it and your own logging goes quiet, which is the intent.

The browser's own violation messages are not emitted through your page's console methods, so they are not suppressed by that option. What is suppressed is everything your code would have said around the failure: the error you caught and logged, the feature-detection result you printed, the fallback you announced. In practice that is most of what a developer reads when narrowing this down, and losing it turns a two-minute diagnosis into a long one. If you are chasing a capability failure in a protected build, turn console suppression off for that build before anything else.

The wider point applies beyond this header. Console suppression is a real cost paid for a real benefit, and the cost falls entirely on the person debugging a production incident. Decide it deliberately rather than inheriting it from a preset.

If you ship a widget onto other people's pages

For a distributed script the header is not yours to set, which changes the job from configuration to documentation. The host page controls its own Permissions-Policy and controls the allow attribute on any iframe your widget uses, so a capability your widget needs is a requirement you have to state and they have to grant.

Write it down explicitly in your integration guide: the capabilities you need, the exact allow value for an embedded frame, and what your widget does when the capability is absent. That last part is the one people skip, and it is the one that generates the support load. A widget that degrades clearly when denied a feature is straightforward to support; one that fails silently produces tickets that arrive as reports that your script is broken.

Feature-detect rather than assume, and test the denied path deliberately. It is easy to arrange, because you can deny a capability to your own test page with a header and watch what your code does. That path deserves the same attention as the granted one, and in a protected build it deserves it more, because a failure there will be harder to read.

The short version

Permissions-Policy and obfuscation operate on different things and do not conflict. The header decides which capabilities a document may use; the transform changes how your code reads. Neither changes the other, and a protected build does not need different policy settings.

What is worth remembering is the asymmetry. A header keeps working when your code has been fully understood and edited by someone else, which is precisely when in-bundle logic stops working. And when a gated feature does fail, protection makes it look like the culprit while console suppression removes the evidence that would clear it. Reproduce unprotected, read the headers on the document that actually runs the code, and keep your logging alive while you are diagnosing.

Frequently asked questions

Does obfuscation break Permissions-Policy?

No. The header is sent by your server and applied by the browser before your code runs, so a code transform has no way to interact with it. A protected build and an unprotected build are subject to exactly the same policy. If a feature is failing, reproduce the same page without protection: if it still fails, the policy or the embedding context is the cause and changing protection settings will not help.

Why does my camera or geolocation call fail only in the protected build?

Usually it does not, and the coincidence is what misleads. The check that settles it is to run the same deployment unprotected, because a policy denial reproduces identically. When the failure really does track the protected build, the cause is normally that the two builds are deployed to different origins or paths that send different headers, or that one runs inside an iframe that was granted the capability and the other does not. Compare the response headers for the document that runs the code rather than for the top-level page.

Is Permissions-Policy a substitute for obfuscating my code?

They answer different questions and neither replaces the other. The header constrains what any code in that document may do, including code an attacker has modified, which is a control that survives the client being fully compromised. Obfuscation raises the cost of reading and reusing your code, which is a different objective. A sensible arrangement uses headers to bound capability, keeps decisions that must be trustworthy on your server, and uses protection to make the client-side work expensive rather than free.

Does console suppression hide the browser's policy violation message?

The browser's own violation reporting does not travel through your page's console methods, so the option does not remove it. What it removes is your own logging around the failure, which is usually what people are actually reading: the caught error, the feature-detection result, the fallback notice. Since that is most of the diagnostic trail, turn console suppression off on any build you are using to chase a capability failure.

How do I handle a capability my embedded widget needs?

Treat it as an integration requirement rather than something you can arrange from inside the widget. The host page controls its Permissions-Policy and the allow attribute on any frame you run in, so document the capabilities you need, the exact allow value for an embedded frame, and the behaviour when the capability is denied. Feature-detect at run time and degrade visibly, because a silent failure in a third-party script is reported to you as your script being broken.

Should I set Permissions-Policy differently for a protected build?

No. The policy should describe the capabilities your application genuinely uses, and protection does not change that set. Deny everything you do not need, which limits what injected or modified code can reach, and grant the rest as narrowly as the syntax allows. Then keep the policy identical across protected and unprotected builds so that a difference in behaviour between them is never explained by a difference in headers.

Related reading