008 Work

Redwood · Customer-facing platform

When one organization could see another's records

Software that serves several organizations from one database has a single job it cannot get wrong: keeping them apart. This system got it right in most places, which is exactly why the gaps were so hard to see.

0FX At a glance

15Places without the check
1Rule, applied everywhere
0Reports from users
Shape
One system, one database, many separate organizations
Found
15 places returned records without an ownership check
Why missed
Most of the software was correct; the gaps looked normal
Fix
One ownership rule, enforced per record handed out
Method
Every route read, not a sample
Reported by users
Never — found before anyone noticed

Most business software is shared. One running system, one database, many separate organizations inside it, each seeing only its own work. Nobody describes it that way to a customer, but it is the arrangement behind almost every product a company buys as a subscription rather than installs.

It comes with one rule. Organization A must never see organization B’s records.

The rule is simple to state. Applying it in every single place is not, and the failure mode is quiet. Nothing crashes. No error appears. Somebody simply sees a list with one row in it that belongs to another company, and most of the time they do not notice, because a name in a list looks like a name in a list.

Why this gets missed

Because the software is mostly right.

Everyone working on a system like this knows the rule. They apply it when they are thinking about it. The gaps appear where they were not thinking about it: a screen added under time pressure, a listing written back when the platform had one customer and the question had no meaning yet, a helper that filters correctly in one place and is called from a second place where the filtering was assumed to have already happened.

None of that shows up when you read changes one at a time. Each change looks fine. The gap is in the relationship between a change and a rule that lives in everybody’s head, and code review does not catch that reliably, ever.

What we found

Fifteen places where a record could be handed out without checking which organization was asking for it.

They clustered exactly where the theory predicts. Detail views — where the software has already been given the identifier of the thing you want, and answers with it. And listings written early in the product’s life, before there was a second customer to keep out.

Nobody had reported it, and nobody had exploited it as far as anything could show. That is not comfort. It means only that nobody had gone looking, and it is the reason this finding is worth more than one that surfaced through a complaint: it was found before somebody else found it, and the difference between those two situations is the difference between a piece of work and a notification obligation.

How it was closed

The wrong fix is to add the missing check in fifteen places. It works, it passes review, and in a year there are seventeen places again — because the sixteenth and seventeenth were written by somebody who did not know the check existed.

So the fix went in one layer down. There is now a single rule that answers one question — is this asker allowed this record? — and it is asked at the point a record is handed out rather than at each of the places that ask for records. New screens inherit it without their author doing anything, and the way to get it wrong is now to deliberately go around it rather than to simply not know.

The other half of the work was the boring half, and it is the one that matters. Every route in the system was read, not a sample. Fifteen is the number because somebody counted all of them, and the only way to know that fifteen is the whole number is to have looked at every single one.

What we would do differently

We fixed the fifteen and we introduced the rule. What we did not build is the thing that keeps the count at zero: an automatic check that fails when a new route hands out a record without going through the rule.

That is buildable. It was out of scope here and it is the obvious next piece, because everything above depends on nobody making the same understandable mistake next year, and depending on that is precisely what got the system to fifteen in the first place.

We also did not go back through the access logs to establish whether any cross-organization record had ever actually been served. The retention window was shorter than the age of the oldest gap, so the honest answer was that it could not be established either way — and we said so rather than implying a clean bill of health that the data could not support.

If this sounds like your system

The question to put to whoever maintains your software is short: when our software fetches a record, what stops it handing that record to a different customer?

There are two good answers. “The check is built into how records are fetched” is the one you want. “Every screen does its own check” is the common one, and it is worth asking the follow-up: how do we know every screen does?

If your product serves several client organizations from one system, this is worth establishing deliberately rather than assuming. Tell us what you run and the reply will say whether it is worth your money.

0SY The symptom this fixes

A questionnaire arrived. Half the questions are about software nobody has looked at in years, and the honest answer to several of them is that you don't know.

0RL Related work

Other work worth reading

Does this sound like your system?

Describe what breaks in your own words. A person reads it and replies within one business day — what we think is happening and whether we’re the right people for it. Free.

Write to us hello@yourcodecare.com