Benefits And Eligibility
Published
A strict check that sits behind an enabling flag is not two controls. It is one control with an off switch, and the off switch is consulted first. This pass measured a benefits enrolment engine where the flag and the verifier can be lost by the same pattern, and found that the configuration kept reporting the strong verifier as installed while it was never once called.
The enrolment, and the switch in front of the verifier
The sample is a benefits eligibility engine. It demands an attestation, and the employer supplies a verifier that checks the attestation against what HR has on record. It applies a sixty-day waiting period from the hire date, an ACA-style full-time threshold of thirty hours a week, and a dependent age limit of twenty-six.
The verifier sits behind requireVerification. The library consults that flag first, and only calls the verifier if it is set. This is an entirely ordinary arrangement -- a strict check and a switch to turn it on -- and it is the arrangement that decides how the pair fails.
Unprotected, a forged attestation is refused with verification-failed(attestation-does-not-match-hr-record), and the four genuine enrolments succeed.
The loud half is switched off by the quiet half
Renaming verifier alone reverts it to the library's built-in, which checks presence and nothing else. The forged attestation is enrolled. The configuration line changes to report a built-in verifier, which is at least a visible trace.
Renaming requireVerification alone means the flag is never set, so the verifier is never consulted however good it is. The forged attestation is enrolled again. Renaming both in one pattern is byte-identical to renaming the flag alone.
The reason this hides from a review is what the configuration reports in the second case. Asked whether a caller-supplied verifier is installed, the engine answers accurately: it is present, it is bound, and it is correct. It is simply never called. A question of the form "is verification configured?" returns a true answer to the wrong question, and any reasoning that begins "if this broke we would know" is only valid when the thing that would tell you is not downstream of the thing that broke.
Two fields, adjacent lines, different casualties
The enrolment record carries an attestation, read by the employer's own verifier, and a weeklyHours, read by the installed library. They are written on adjacent lines of the same object literal and nothing about them suggests they would behave differently.
Renaming attestation moves both the write and the read for records the sample builds in code, so those enrolments are unaffected. The record that actually arrives -- parsed from the enrolment portal as JSON -- is refused: verification-failed on a genuine, correctly attested enrolment. The employees who submitted through the real path are locked out.
Renaming weeklyHours does the reverse. The library's read does not move, so the in-code records report not-full-time(undefinedh < 30h) and every genuine full-time employee is refused, while the record from the portal is untouched and enrols normally. Same object, adjacent lines, and the casualties are opposite sets of people.
Both of these fail closed, and that is not luck
In an earlier pass this same shape produced one field failing closed and one failing open, on one line of one literal. Here both fail closed, and the reason is in the comparison rather than in the fields. The full-time check refuses when the hours are not greater than or equal to the minimum. Against an unreadable value that comparison is false, the negation is true, and the enrolment is refused.
Compare that with a leave-balance check measured the same pass, which refuses when a request exceeds the balance. Against the same unreadable value that comparison is also false -- but there false means do not refuse, so the request is granted. A ceiling test fails open and a floor test fails closed, and the direction has nothing to do with which field is more important.
This is a useful thing to be able to predict about your own code. Before assuming a threshold will fail safe, read whether it asks for sufficiency or for excess. The sufficiency phrasing is the one that refuses when it cannot tell, which is usually what you want from an eligibility rule.
The rest of the eligibility surface
Renaming minWeeklyHours removes the full-time determination entirely, so a twelve-hour-a-week employee is enrolled in a plan they are not eligible for -- which is a carrier billing problem and, in the United States, a compliance one. Renaming waitingPeriodDays enrols a hire from their first day.
Renaming hireDate defeats the waiting period from the record side rather than the option side, since that field is read by the installed library. Renaming dependentMaxAge or the age field on the dependent rows keeps a thirty-one-year-old dependent on the plan; the enrolment still succeeds either way, and the only thing that changes is who is covered, which makes it among the quietest results in the file.
Renaming valid -- the field the verifier returns its decision in -- refuses every enrolment including the genuine ones. The verifier runs, decides correctly, and the engine cannot read the answer. That is a permission-shaped flag, and a missing permission reads as no, so this one fails closed and is discovered immediately.
What protection alone did, and what to do about it
Nothing. The file was protected on five profiles -- the ES5 default, the modern target, both emit-gate configurations and the string-encoding profile -- and produced byte-identical output on all five. Every enrolment and refusal matched. Renaming identifiers and encoding strings does not disturb an eligibility engine.
Every result above required member renaming with a pattern naming those members. Keep it anchored to names your own code owns on both sides. Both the options object and the enrolment records are read by installed code, so both are contracts.
For the enabling-flag pair specifically, the design mitigation is to make the strict path the default and the relaxation the explicit setting, so a lost name removes the relaxation rather than the check. And when reporting on a control, report whether it ran, not only whether it is configured -- a verifier that is installed, bound, correct and never called should not be able to answer yes to the only question anyone asks it.
Frequently asked questions
Does obfuscating a benefits platform change who is eligible?
Not by itself. The sample was protected on five profiles including the ES5 default, the modern target and string encoding, and produced identical enrolment decisions on all five. Every result in this article required member renaming, which is off unless you enable it and supply a pattern.
What happened when the enabling flag in front of the verifier was renamed?
The verifier was never consulted and a forged attestation was enrolled. Renaming the verifier itself produced the same outcome by a different route, and renaming both together was identical to renaming the flag alone.
Why is that hard to catch in a review?
Because the configuration still reports a caller-supplied verifier, which is accurate. The verifier is present, bound and correct. It is simply never called, so asking whether verification is configured returns a true answer to the wrong question.
Two fields on adjacent lines behaved differently. Why?
Because they have different readers. The attestation is read by the employer's own verifier, so a rename moves the write and the read together and only the record arriving as JSON breaks. The weekly hours are read by the installed library, so the write moves and their read does not, and the in-code records break instead.
Did any of these fail open?
The enabling-flag arms did: a forged attestation enrolled. The threshold arms failed closed, because the full-time check asks whether hours are sufficient, and sufficiency that cannot be established reads as insufficient.
How can I predict which way one of my own thresholds will fail?
Read whether it tests for sufficiency or for excess. A floor test asking whether there is enough fails closed. A ceiling test asking whether there is too much fails open, because the excess it watches for cannot be established.
What is the design fix for a check behind an enabling flag?
Make the strict path the default and the relaxation the explicit setting, so losing a name removes the relaxation rather than the control. And report whether a check ran, separately from whether it is configured.
Related reading