Approvals And Controls

Does obfuscation break timesheet approval controls?

The rule against approving your own timesheet is not a policy engine or a permission system. It is two field reads and an equality test, and it is the only thing between an employee and signing off their own overtime. This pass renamed the field it compares, and measured the same sheet twice: once as it arrives from the database, and once as the application rebuilds it in memory.

The control, and how it is actually written

The sample is an approval workflow of the kind every time-tracking product ships. A relationship denylist forbids self and peer approvals. An approver allowlist names the three people who may sign anything off. Approved sheets are locked against later editing. No single entry may claim more than sixteen hours. Unapproved sheets are not payable.

The self-approval rule itself is the plainest code in the file: compare the sheet's own submittedBy field against the person approving, and refuse if they match. It is not a lookup, not a role check, and not a list. It is an equality test between two strings, and it is the control an auditor would name first if asked what stops an employee paying themselves.

The sheet that matters carries two nine-hour days of overtime, submitted by the employee who is also trying to approve it. Unprotected, it is refused with self-approval-is-not-permitted(emp-77).

The same sheet, twice, with the same values

The experiment presents that sheet in the two forms it genuinely takes. One is parsed from stored JSON exactly as it came out of the time-tracking database. The other is rebuilt in code from the same values, which is what an application does when it hydrates a row into its own shape before handing it to a library.

Renaming submittedBy leaves the stored sheet refused, with the original reason, in every arm. Keys inside parsed JSON are text; they arrive spelled the way they were serialised and no rename touches them. The rebuilt sheet is a different matter: the property the application wrote has moved, the equality test compares an undefined against the approver, the two are not equal, and the self-approval rule has nothing to say.

That single rename did not immediately produce an approval, because a second control caught it: the relationship denylist reclassified the approver as a peer and refused on those grounds. The reason string moved from self-approval-is-not-permitted to forbidden-relationship(peer), and the outcome did not change. Read alone, that is a cosmetic finding.

Three controls, one pattern

Adding forbiddenApprovers to the pattern moved the refusal again, to approver-not-on-allowlist(emp-77). Still refused, still only a label change, still the kind of result a review closes. Adding approverAllowlist as well produced APPROVED=true approved(emp-77 signed off ts-501 as peer).

The employee approved their own overtime timesheet. The approval is recorded as an ordinary peer sign-off, indistinguishable in the record from any other. Three separate controls -- a self-approval rule, a relationship denylist and an approver allowlist -- were each doing real work, and all three were reached by one renaming pattern because they are properties of one options object in one file.

Throughout, the sheet that arrived as stored JSON was refused in every single arm. Identical values, identical rule, opposite outcome, and the only difference is which side of the boundary the field names were written on.

Why the in-memory record is the exposed one

This inverts the intuition most reviewers bring. Persisted data feels like the hazard: a record written by an old build and read by a new one. The measurement says the opposite. A stored record's keys are text and survive any rename, so a guard reading them keeps working. The record your own code assembles carries names, and names move with the transformation.

That makes the exposure the hydration step -- the place where an application copies a database row into its own object literal before handing it to an installed library. It is the least interesting code in the file, it is usually written once and never revisited, and it is where the contract lives.

The corollary is worth stating for reviews: a guard that reads fields off a record cannot be assessed without knowing how that record was constructed on the path being tested. A test that builds the record inline may measure the rule as working, truthfully and uselessly, because both the write and the read moved together.

The rest of the controls

Renaming approvedAt unlocks a sheet that was already approved: the lock checks for that stamp, cannot find it, and the edit proceeds. The measurement rewrote an approved eight-hour entry to eighteen hours after sign-off, which is exactly the shape of the after-the-fact addition that approval workflows exist to prevent. Renaming lockAfterApproval reaches the same outcome by disabling the lock instead.

Renaming hours or entries defeats the per-entry ceiling, so the twenty-two-hour day is approved. Renaming requireApproval reverts it to the library default, making unapproved sheets payable. Renaming reportsTo reclassifies every genuine manager as a peer, which the denylist then refuses -- a failure that stops the workflow entirely and is therefore discovered at once.

The pattern across the file is the familiar one: the arms that break the workflow are found within a day, and the arms that quietly permit something are the ones that persist.

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 approval and refusal matched. Renaming identifiers and encoding strings does not disturb an approval workflow.

Every result above required member renaming with a pattern naming those members. Keep it anchored to names your own code owns on both sides, and treat both the options object and the record fields you hand across as contracts.

For a control that matters to an auditor, do not let the client build be where it lives. Re-evaluate the self-approval rule server-side against the stored row, where the field name is text rather than an identifier. Then assert the specific case in the tests: the author of a sheet, approving that sheet, must be refused -- and build the record on the path the application actually uses, not inline in the test.

Frequently asked questions

Does obfuscating a time-tracking app weaken its approval controls?

Not by itself. The sample was protected on five profiles including the ES5 default, the modern target and string encoding, and produced identical approvals and refusals on all five. Every result in this article required member renaming, which is off unless you enable it and supply a pattern.

Why was the same timesheet refused in one form and approved in another?

Because of how the record was built. The sheet parsed from stored JSON keeps its field names as text, so the self-approval comparison still works. The sheet rebuilt in code carries names that move with the rename, so the comparison reads undefined and finds no match.

Did renaming the self-approval field immediately allow a self-approval?

No. A relationship denylist caught it and the refusal reason changed instead. Only when the denylist and the approver allowlist were included in the same pattern did the employee's own sign-off succeed.

So is stored data safer than in-memory data here?

For this failure mode, yes, which inverts the usual intuition. Keys inside parsed JSON arrive spelled as written and no rename touches them. The exposed surface is the hydration step where your code copies a row into its own object.

What does a successful self-approval look like in the record?

An ordinary sign-off. The measurement recorded it as a peer approval of the sheet by its own author, with nothing in the entry to distinguish it from a legitimate approval by somebody else.

Can an approved timesheet be edited afterwards?

If the field carrying the approval timestamp is renamed, yes. The lock looks for that stamp, cannot find it, and permits the edit. The measurement rewrote an approved eight-hour entry to eighteen hours after sign-off.

How should the test for this be written?

Assert that the author of a sheet approving their own sheet is refused, and construct the record the way the application actually constructs it. A test that builds the record inline can report the rule as working even when the shipped path no longer enforces it.

Related reading