Threat Model

Can attackers diff your releases to find the fix?

You ship a fix for a client-side issue. The patched bundle is public by construction, and so is the one it replaced, because anyone who was paying attention kept a copy. Comparing the two is the cheapest way to find out what you just repaired, and whether that comparison tells them anything depends on a build setting most teams never think about.

The setup is unusual, and it favours the attacker

Server-side software has an asymmetry that client-side software does not. When you patch a service, the fixed code stays on your infrastructure and an outsider sees only the changed behaviour. When you patch a browser bundle, you hand them the artefact. Both artefacts, in fact, because anyone who kept yesterday’s file now holds the before and the after.

That is the classic patch-diffing setup, and it is the reason the question comes up at all. The attacker is not trying to understand your whole application. They are trying to find the small region that changed between two releases, on the reasonable assumption that a change shipped shortly after a security advisory is the fix, and that the code around it describes the flaw.

It is worth being precise about who this concerns. It is a real consideration for software with an installed base that updates on its own schedule, where an unpatched version keeps running for weeks. It matters much less for a web application that everyone reloads within a day, because the window in which knowing about the flaw is useful has mostly closed by the time the analysis finishes.

What the default output does to the diff

The relevant fact is a default rather than an option you have to find. Protection output is polymorphic per build unless you tell it otherwise: identifiers are regenerated, the string table is rebuilt, and layout decisions come out differently each run. The configuration schema states the contract directly, describing the seed setting as producing byte-identical output for the same input, options and seed, and noting that omitting it gives you the default per-build polymorphic output.

Run that through a diff tool and the result is close to useless for locating a change. Nearly every line reports as modified because nearly every name in it is different, and the region that actually changed is not distinguishable from the regions that only look changed. The signal an attacker wants is buried in noise that your build produced for free.

Be careful about how much to claim for this. It defeats a textual comparison, which is the cheap and most common method. It does not defeat a comparison of behaviour, where someone runs both versions against the same inputs and looks at what differs in the output, the network traffic or the observable state. That method never depended on your names being stable, so it survives, and it is what a determined analyst reaches for once the easy route fails.

Four things that want the opposite

If regenerated output were free, this article would be one paragraph. It is not, because several workflows you may already depend on want output that is stable across builds, and they are more concrete than the threat they trade against.

Reproducible verification. A fixed seed is what lets someone rebuild your release from the same inputs and get the same bytes. That is the basis for a verifiable release record, and it is exactly the property that turns a claim about what you shipped into a result somebody else can regenerate. Reproducible builds and the seed covers the mechanics.

Delta updates. Applications that ship bundle updates over the air send a binary difference against the installed version. Regenerate every identifier and consecutive bundles diverge everywhere, so the delta approaches the size of a full download and the update economics you designed for stop working. The over-the-air update article works through that case.

Long-lived caching. A file whose contents change on every build gets a new hash on every build, so returning visitors re-download code that did not change. Obfuscation and long-term caching describes the cost and how to split bundles so it lands where it hurts least.

Diagnosis. Two builds of the same source that produce different symbols make it harder to reason about a report from the field, because the mapping you need is specific to the build the user ran rather than to the release. This is manageable with per-build records, but it is friction, and it is friction during an incident.

Notice that the first three are requirements somebody in your organisation has already asked for, while resistance to release comparison is a defensive posture against a threat that has alternative routes. That asymmetry should inform the default.

Splitting the decision instead of making it once

The setting is per build, so the useful move is to stop treating it as one organisational policy. Most applications have a small amount of code where the logic is genuinely sensitive and a large amount where it is not, and those two halves want different answers.

Put the sensitive part in its own bundle and leave it unseeded, so each release regenerates and a comparison against the previous release yields nothing usable. Seed the rest, so your reproducibility records, your delta updates and your cache lifetimes keep working across the bulk of the artefact. This is the same shape as the split recommended for detection code, where per-build regeneration is the entire benefit and a fixed seed would be a mistake.

Record which bundles are seeded and why. The reason this matters is that the property is invisible in the output: nobody can tell from a protected file whether its stability was intentional. A line in your build configuration and a note in your release documentation is the whole cost, and it prevents the decision from being reversed by accident during an unrelated change.

The part that actually reduces the value of a diff

There is a more effective answer than making the fix hard to find, and it is the one that keeps appearing across this site: make finding it not worth much.

If the vulnerability was that the browser was trusted to decide something, then locating the patched line tells an attacker which check to bypass, and bypassing a client-side check does not require understanding it. Moving the decision to the server does not merely fix that instance, it removes the category, because after the move the client code contains no decision to find. A diff of your bundle then shows a changed interface and nothing of consequence.

Where the flaw is genuinely browser-side, the honest answer is that speed beats concealment. The window that matters is between your release and the point at which most installations have taken it, and that is a distribution problem rather than a protection one. Anything that shortens it, such as forcing an update check or invalidating a cached bundle, is worth more than making the comparison harder.

The short version

Shipping client-side code means shipping both sides of your patch, and comparing them is the cheapest way to locate a security change. Default per-build output makes a textual comparison close to worthless, which is a genuine benefit you get without configuring anything, but it does not stop behavioural comparison and it does not stop someone testing your endpoint directly. A fixed seed reverses that benefit, and it is the correct choice for reproducibility, delta updates and caching, so decide it per bundle rather than once for everything. Then put the durable fix where the client cannot reach it.

Frequently asked questions

Does obfuscation stop someone comparing two of our releases?

It changes what a comparison yields rather than preventing one. With the default per-build output, every identifier and every string-table layout is regenerated on each run, so a textual diff of two protected bundles reports that almost everything changed and points at nothing in particular. Someone can still compare behaviour by running both versions, which is a slower and noisier method but a real one. Treat the effect as raising the cost of locating a change, not as removing the ability to find it.

If we set a fixed seed, are we making patch diffing easy?

You are making it far easier than the default, yes. A fixed seed means the same input, options and seed produce byte-identical output, so two releases differ only where the source differed and a diff points straight at your change. That is precisely why the seed is useful for reproducible builds and release records, and it is a real trade against this concern. The resolution is usually per bundle rather than per project: the parts of your build that carry the sensitive logic can be unseeded while the rest stays reproducible.

Which is the right default, a fixed seed or regenerated output?

Start unseeded, because that is the default and it costs nothing, then add a seed where a specific workflow needs one. Reproducible verification, delta updates for shipped applications, and long-lived cache entries all want stable output and are concrete engineering requirements. Resistance to release comparison is a diffuse benefit against an attacker who has other routes. Deciding per bundle rather than per organisation keeps both, and the decision belongs in your build configuration where it is reviewable.

How else would someone find the fix if they cannot diff the bundle?

Several ways, most of them cheaper than reading code. Your advisory or release note may describe the class of issue. The behaviour change itself is observable by using the application before and after. If the flaw was reachable over the network, the endpoint can be probed directly without reference to any client file at all. That last route is the important one, because it is unaffected by anything you do to the bundle and it tells you where the fix actually needed to land.

Does this mean a client-side fix is worthless?

No, it means a client-side fix should not be the only one. If the flaw was a validation gap, a missing check or an exposed capability, the durable repair is on the server that receives the request, and the client change is a user-experience improvement on top of it. Where the issue is genuinely browser-side, such as a rendering path that mishandles untrusted content, the client fix is the fix, and then the questions in this article apply properly.

Should we delay publishing the release note to slow down comparison?

That trade rarely pays. Users and downstream integrators need to know what changed in order to decide how urgently to take an update, and withholding that pushes cost onto the people you are trying to protect. A better shape is to publish the fact and the urgency promptly while keeping the reproduction detail for a later disclosure window, which is the practice most coordinated disclosure processes already assume.

Related reading