Entitlement And Licensing
Published
Entitlement is the layer a software vendor's own revenue sits on, and it is the layer most often shipped inside the client bundle. Which tier a feature belongs to, how many seats a contract allows, whether the licence in front of it is genuine -- all property names. We protected a file that configures an entitlement library, renamed the names a group at a time, and counted what was granted. The result includes the most precise finding of this pass: two fields on the same object, written by the same line, failing in opposite directions.
The configuration under test
The sample is a vendor's own gate. A licence is required. The application supplies a real verifier that checks a signature against the vendor key. Two plans are defined, Basic granting one feature and Pro granting three. The contract is five seats, with a seven-day grace period after expiry.
Unprotected it behaves: the Pro customer gets SSO, the Basic customer is refused the Pro-only bulk API with not-in-plan(basic), a hand-forged licence is refused with signature-does-not-match-vendor-key, a licence that lapsed in June is refused as expired, no licence at all is refused, and the ninth person on a five-seat contract is refused with over-seat-limit(9>5).
The quiet half switches off the loud one
Two names guard the licence. requireActiveSubscription decides whether the check runs at all, and verifyLicense is the strict check itself. That ordering is deliberate and ordinary -- a developer tool has to be usable offline -- and it is exactly what makes the pair dangerous.
Renaming verifyLicense alone substitutes the library's built-in verifier, which checks presence rather than signature. The forged licence is accepted. But a request with no licence at all is still refused, so the arm looks partially protective and reads as a downgrade rather than a hole.
Renaming requireActiveSubscription alone is worse and quieter: the enforcement line reads no(the build runs unlicensed) and everything is granted -- the forged licence, the lapsed licence, and no licence at all. Renaming both together is behaviourally identical to renaming the flag alone. The half that would have refused the empty licence is switched off by the half that fails silently, because the flag is consulted first and the strict verifier is never reached. Any reasoning of the form 'if this broke we would know' only holds when the thing that would tell you is not downstream of the thing that broke.
The finding this pass turns on: who reads the field decides the direction
The licence object carries a signature and an expiresAt, written on the same line of the same literal. They fail in opposite directions, and the reason is not their importance.
signature is read by the application's own verifier, in the same file that writes it. Renaming moves both sides together, so with a licence built in code the arm measured no change at all -- it looked safe. It was not safe; it was blind. Adding the licence as it actually arrives, parsed from a file, exposed it immediately: the parsed record's keys are text written by whoever issued the licence, so only the read moved. The genuine, correctly signed licence was refused with signature-does-not-match-vendor-key. Every paying customer locked out, loudly, on the first build.
expiresAt is read by the installed library, not by our code. Renaming moves our write and leaves their read untouched, so the library sees undefined, parses it to NaN, and the expiry comparison fails false. The licence that lapsed in June was granted. Same object, same line, one field fails closed and the other fails open, and the deciding factor is which side of the boundary the reader sits on.
Seats, plans, and the ordinary revenue leak
Renaming seatLimit reverted the limit to unlimited and granted the ninth person on a five-seat contract. Nothing threw, the reported limit read (none - unlimited seats), and the overage that a renewal conversation is normally built on simply stopped being recorded. Renaming plans or the features arrays inside it goes the other way and refuses everything with not-in-plan, which is loud.
Renaming valid, the field the vendor's own verifier returns its decision in, refused every licence including the genuine one -- and printed the reason the verifier gave for accepting it. A permission-shaped flag that goes missing reads as 'no', which is why phrasing a collaborator's answer as a permission rather than a prohibition is worth doing at design time. It costs nothing and it decides whether losing the field opens the door or closes it.
The control arm behaved as predicted: checkIntervalMs was pinned to the library's default, so renaming it changed nothing. Fourteen consecutive passes that prediction has held exactly.
What protection alone did
Nothing. Five profiles, byte-identical grants and refusals. Protecting an entitlement file does not weaken it -- which is worth saying clearly, because this is the one area where the honest advice about client-side enforcement can be mistaken for advice against protecting it at all.
Obfuscation raises the cost of reading and patching a client-side licence check, and that is a real and useful thing. It does not turn a client-side check into a server-side one.
What to do about it
Keep renaming patterns off the entitlement configuration and off the licence body. The licence body is the one people miss, because it reads as data; the measurement above shows two fields on it failing in opposite directions.
Where a feature has revenue attached, have the server decide. A client-side gate is a deterrent and a convenience, and it should be backed by an entitlement the API enforces on every call that matters. That is the difference between a customer having to work to bypass a check and a customer's bypass having no effect.
And write the flags your own guards return as permissions, never as prohibitions. A missing permission reads false and refuses; a missing prohibition reads false and allows. This is free at design time and it is the difference between a locked-out customer and an open door.
Frequently asked questions
Does obfuscating a licence check weaken it?
No. Protected on five profiles, the sample produced identical grants and refusals. Obfuscation raises the cost of reading and patching a client-side check; the failures in this article all required member renaming aimed at the entitlement library's own names.
Why did renaming the licence signature field look safe at first?
Because the test licence was built in code, so the write and the read moved together. With a licence parsed from a file -- which is how a licence actually arrives -- only the read moved and the genuine licence was refused. An arm that measures no change is not evidence until you have run the operation the name governs.
How can two fields on the same object fail in opposite directions?
Because a different party reads each one. The signature is read by our own verifier, so both sides move together. The expiry is read by the installed library, so our write moves and their read does not, the date parses to NaN, and a lapsed licence is granted.
What happens when the enforcement flag and the verifier are both renamed?
The same thing as renaming the flag alone: everything is granted, including a request with no licence at all. The flag is consulted first, so the strict verifier is never reached. The loud half is switched off by the quiet one.
Does renaming a seat limit matter?
It reverted to unlimited and granted the ninth user on a five-seat contract. Nothing threw and nothing was logged; the overage a renewal conversation is normally based on simply stopped being recorded.
Should the flag our guard returns be a permission or a prohibition?
A permission. A missing permission reads false and the answer becomes no, which fails closed. A missing prohibition also reads false, and the answer becomes allow. Renaming, a version bump, a serialisation hop that drops undefined keys and a hand-written mock all produce the same undefined.
Is client-side entitlement ever enough?
For convenience and for deterrence, yes. For anything with revenue attached, the API should enforce the entitlement on every call that matters, because the customer controls the browser and every client-side check is ultimately advisory.
Related reading