Engineering··5 min read

The Door Nobody Counted

Practice mode skipped the record, so it looked harmless. It also opened today's questions and revealed the answers. Three practice runs bought a perfect score.

#Access Control#Daily Games#DUGOUT 9

DUGOUT 9 is an independent fan project for KBO, the Korean professional baseball league, published as a public demo. Part of it is three daily games, rebuilt each morning from official career records: guess the player from their team history, call a stat higher or lower, find the one who does not belong. Everyone gets the same three questions on the same day, results go into a season score and a streak, and there is a ranking.

There was also a practice mode. The idea was reasonable: let a fan warm up without it counting. A request could say this is practice, the server would grade the answer and skip writing the record. Since nothing was recorded, I treated it as the loose corner of the product. Nothing to protect, because nothing was at stake.

What practice actually handed out

Practice opened today's questions. The same three. And when you got one wrong, it told you the correct answer — the natural thing for a practice mode to do.

Put those together and the game had no game. Three practice runs and you knew today's answers. Then play for real and take a perfect score into the ranking. The mode I had marked as harmless because it wrote nothing was the one place where the product's only valuable thing — the answer — was given away to anyone who asked.

"It doesn't score, so it's safe" had the logic backwards. It was unsafe because it did not score. An unrecorded request is one nobody counts, nobody rate-limits, nobody notices. The absence of a write was not a reason to relax; it was the reason the door stood open with no one at it.

Practice became the past

The fix was to change what practice means. It is no longer a request that says it is practice. It is opening a question from a past date. If the question has nothing to do with today's answers, revealing the answer costs nothing, so practice can be as generous as it likes. Today's questions are graded only on requests that will be recorded, and the convenient ungraded path to them was removed rather than guarded.

This had a side effect I was glad of. The archive became real. Before, every link to a past date quietly opened today's questions — the archive was a list of dates pointing at the same three items. Now a past date opens that day's games, marked as practice, outside the season score and the streak.

Then the archive leaked too

The fix itself was wrong once. While opening the archive I reasoned "past dates, so open as many as you like" and set the window to four hundred days.

The three games are generated in sequence from a finite pool of players. Walk far enough back through the sequence and it repeats — every twenty-three days, the same answer returns. So opening the question from twenty-three days ago was another way of reading today's answer. The door I had just closed had a second copy, four hundred days wide.

The window is two weeks now. Not because two weeks is a nice number, but because it is shorter than the period on which answers recur. The length of an archive is not a convenience setting. It is set by the repetition period of what the archive contains.

Divide access by what comes back, not by what gets written

The general rule I took from this is about where to draw permission lines. The instinct is to classify requests by what they record: writes are dangerous, reads are safe, unlogged reads are safest of all. That instinct is fine for a product whose value is in what it stores.

It is wrong for a product whose value is in what it returns. Games, exams, draws, anything where the answer is the asset — there, the response is the thing being protected, and a request that returns it without counting is not a harmless path but the most dangerous one, because it makes every rule on the counted path meaningless.

And generated content is never as fresh as it looks. If questions are drawn from a finite source, "the past" and "the present" are the same content on different dates, and the distance between them is a number you can compute. If you do not compute it, a door into the past is a door into today.