Three hardenings from the security audit - #66
Merged
Conversation
POST /forgot was the one password door without the per-caller brake: the mail was capped per account, but a stranger could knock at the route itself as often as the wire allowed, each knock a database lookup. It now spends the same door bucket the sign-in and the site password share, and the answer reads the same either way.
The counter asked its repeat question (one query) before it asked the flood limiter, so a caller past the limit still bought a database read on every knock. The limiter now answers first through the new non-counting over?/2, and the slot is spent only by allow? when a row is really written, so a reader who reloads still never loses a slot to the reload.
A lock process lived on after its text was free, so the supervisor carried one process per entry ever opened and grew without bound on an installation that runs for years. Once the text is free and nobody waits for a flush, the process stops; the next editor starts a fresh one on demand. A takeover still in flight keeps it alive until the transfer is done. Reading the state of a lock that ended a breath ago hit the registry's lagging entry, so state/1 answers :free when the pid it finds is gone.
The lock process ends itself once its text is free. A caller can reach it a breath too late: the registry still names the pid, or the pid is alive but already on its way out. The call then exits with :noproc or with :normal, and only the first was caught, and only in state/1. Every call now goes through one helper that catches both and knocks once more on a fresh process. state/1 answers :free instead.
A watcher closing its tab releases a lock it never held. Going through ensure/1 started a process only to end it at once. release/2 now looks the process up and answers :ok when there is none.
Collaborator
Author
|
Review of the three hardenings. The forgot door and the flood turn-away are correct and small. The self-ending lock process had one gap, fixed on the branch. Fixed
Left as is
Full suite after the fixes: 1431 tests, 0 failures. `mix compile --warnings-as-errors` clean. Reviewed with Claude Code, model claude-fable-5. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A security audit of the codebase found no critical and no high findings, and three low ones. This closes those three. Nothing changes for a normal reader or writer; the screens stay as they are.
The forgot form now knocks at the same door as the sign-in. POST /forgot was the one password door without a per-caller brake. The mail was capped at one per account per minute, but the route itself took unlimited knocks, each one a database lookup. It now shares the door bucket that the sign-in and the site password use.
A flood of view counts costs the database nothing. The counter ran its repeat query before the flood limiter, so a caller past the limit still bought one database read per knock. A new non-counting RateLimiter.over?/2 turns a flood away at the door. The slot is still spent only by allow?/2 when a row is really written, so a reader who reloads never loses a slot to the reload, as before.
An empty lock process ends itself. A lock process lived on after its text was free, so the supervisor carried one process per entry ever opened and grew without bound on an installation that runs for years. The process now stops once the text is free and nobody waits for a flush; a takeover still in flight keeps it alive until the transfer is done. Reading the state of a lock that ended a breath ago hit the registry's lagging entry, so Lock.state/1 now answers :free when the pid it finds is already gone.
Tests (all written first, watched failing): the shared-door test hammers /forgot and expects the next sign-in to read Too many tries. The flood tests expect no database query from a caller past the limit, measured over the repo's own telemetry, and expect a fresh page to still count after a minute of reloads. The lock tests watch the process die on release, on idle, and stay alive while a takeover resolves. Full suite: 1428 tests, 0 failures. mix format, compile --warnings-as-errors green. No UI changed, so no screenshots; no reader-visible sentence changed, so no gettext run. (mix gettext.extract --check-up-to-date is already red on main, from newsletter.html.heex line drift in #65. This PR touches no template and stays out of it.)
The fourth audit finding, media URLs living outside the site gate, is a design decision and stays open for a separate discussion.
Worked with opencode, model z-ai/glm-5.3-flash.