Skip to content

Fix/high findings - #222

Merged
SauravYadav12 merged 27 commits into
mainfrom
fix/high-findings
Nov 10, 2026
Merged

SauravYadav12 merged 27 commits into
mainfrom
fix/high-findings

Conversation

@SauravYadav12

Copy link
Copy Markdown
Collaborator

No description provided.

SauravYadav12 and others added 27 commits November 10, 2026 14:52
Creating a branch starts by removing the container of that name, so a
branch called standby killed the live HA standby. CheckNewName refuses
main, standby, restore, restore-check, agent-* and *-before-* wherever a
name comes in from outside: POST /api/branches (masked too), every import
target and its default, pipeline targets, and fox branch create. The
engine's own creation goes through Create and is unaffected.

Create and RestoreExport also refuse when a container of that name is
already there, which the storage check cannot see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…F-3)

Before authenticating, a client's claimed length was enough to get up to
16 MiB allocated per message and 1 MiB per startup packet. The password
message is now capped at 1 KiB and the startup packet at 10000 bytes,
Postgres's own limit, both checked on the header and answered with
SQLSTATE 08P01. Reads from the backend keep their limit and the proxied
session is untouched.

A SASLContinue shorter than its 4-byte code no longer indexes out of range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client.Release fetched and parsed SHA256SUMS without ever looking at
SHA256SUMS.sig, so fox setup installed an engine (which runs as root) on
checksums alone and then printed that it was verified. Only Resolve checked.

Both now read checksums through verifiedSums, which verifies the signature
before parsing. A missing or bad signature, or a build with no release key, is
an error: refreshEngineBinary keeps the installer's engine, and FOX_NO_REFRESH
and FOX_NO_VERIFY stay the only deliberate ways round it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The distro workflow is about to publish SHA256SUMS.distro instead of adding to
SHA256SUMS. The Windows installer looks the image up there first and falls back
to SHA256SUMS, which is where every earlier release lists it. The new listing
only ever answers for the distro image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-checksums.sh downloaded the published SHA256SUMS, merged its own lines
in and signed the result, so anyone able to upload a release asset could get
any hash signed with the release key.

Each job now signs only what it hashed from its own build: release.yml owns
SHA256SUMS, which still lists every file fox update and install.sh need, so
released clients are unaffected, and wsl-distro.yml, which runs on its own
schedule, publishes SHA256SUMS.distro. wsl-zfs.yml stops rewriting SHA256SUMS,
which it did without the key, breaking the signature; no client reads the ZFS
bundle's checksum, so it now sits in a .sha256 file beside the bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ForgetBranch cleared the owner but left the row in pipeline_targets, so a
pipeline's next run deleted whatever another account had since made under
that name, and took it over. ForgetBranch now drops the claim, and a run
replaces an existing target only if the pipeline made it and its user is
still the recorded owner. fox pipeline run refuses a target an account
owns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dispatcher is handed engine names by its own tests and has no reason
to judge them. fox pipeline run now refuses a reserved --as before the
engine deletes that name, as the API already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EnsureRunning and BackendAddr asked docker for the state twice and the IP
once on every connection, each under the ten-minute tier. One inspect now
answers both, on the probe tier, and a branch seen serving is kept for 2s
with concurrent callers sharing the call.

Every docker command that changes a container invalidates it where
commands are run. Only serving is remembered, so no stale absent or
stopped can exist; waking and recreating stand on a read of their own, and
a docker that did not answer is ErrStateUnknown, never absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gateway no longer asks for a branch's state before EnsureRunning asks
again. A remembered address can be left behind by another process's
suspend, so a failed dial forgets it and routes once more, which wakes a
suspended branch without the client seeing an error. The console's
sessions do the same. A docker that did not answer tells the client to
come back rather than that the branch does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An entry's statement is the source's whole current_query(), which can hold a
COMMIT, a SET ROLE or a SET bb.actor beside the change it was recorded for.
Replayed onto the target, the first ended the one transaction a request is
applied in and the others changed who the rest ran or was recorded as.

Such a request is now refused when it is asked for and again when it is
applied, from a Go reading of the message that follows the server's scanner,
comments, quoting and continuations included. What it cannot read is refused.

Audit TX-5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e lock

A transaction in REPEATABLE READ or SERIALIZABLE read the tail through a
snapshot that could be older than the append lock, so two rows chained off
one parent and verify reported tampering. Ids came from the column default,
before the lock, so id order and chain order could differ.

bb.ledger_head holds the last id and hash, taken with UPDATE ... RETURNING
under the lock: a stale snapshot now gets a serialization error instead of
a fork, ids follow the chain and a rolled-back entry leaves no gap. Seeded
from the tail on an existing ledger, without touching a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CheckpointSecurityLog reloaded and re-hashed every event on each checkpoint, so its cost followed the log's whole length. It now checks the last anchored event against its anchor and chains forward from it, a page at a time.

The full check stays where it was: fox audit verify still reads every event against every anchor, and only that catches an old event edited in place. The doc comment and a test say exactly where the line is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every invalid key was a chained row written under the write lock in a log that is never trimmed. Refusals now coalesce to one event per address per five minutes, carrying a count of what was suppressed, and an address that has used up its invalid keys is refused before any key is looked up.

Only invalid keys count towards that, per address, so one address cannot lock out another and a valid key refused for its scope does not cost a client its connection. No retention or deletion is added: the log stays append-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request was applied by psql over stdin in a superuser's session, confined by
SET LOCAL ROLE. psql read every statement as input of its own, so :names and
backslash commands were live, and a statement that left the confinement had
the superuser behind it.

It now logs in as db_client, the role that owns what clients make, and runs in
pgx.BeginFunc, each statement one message that only the server reads. The
guardrail, the policy gate and the Blackbox fire as for any client, the actor
still comes from bb.actor, and a failed apply says truly what was committed.

Audit TX-5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each dashboard poll walked the branches, an inspect and a psql exec apiece,
serially and per client. One read now serves everybody for 5s, single
flight, out of date as soon as this process changes anything. Branches uses
one docker inspect for all states and counts connections over the network,
falling back to the exec.

Access filtering stays after the shared list, per request. Docker's reads
behind the snapshot are on the probe tier, and a caller with an earlier
answer waits at most 4s for a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard and the import page polled on a fixed interval whether the
last request had come back or anyone was looking. The next poll is now
timed from the end of the last, a hidden tab does not poll, and a user's
own action still refreshes at once; the newest refresh wins, so a poll
already out cannot put the old list back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A blocked statement is recorded through a second connection, and a wait
inside it is one Postgres cannot see as a deadlock: another transaction
holding the Blackbox's append lock hung the refused client indefinitely.
The guardrail, the data guards and the policy gate now go through a
connection with lock_timeout=2s and statement_timeout=5s, and a failure to
record is a WARNING: the refusal still happens.

Client logins get lock_timeout=60s and idle_in_transaction_session_timeout
=10min by default, so an abandoned transaction cannot stop the record, set
once per role so an administrator's values stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadAudit prepended each day to everything read so far and re-joined the lot, so a rare match walking many days did quadratic work and held every record. It now chooses the records to show in one pass, holding at most Limit, and reads those days again only for the keys of the statements chosen. The answers are the same as before, including a transaction filed days away from its statement in either direction, which is why this is a second pass and not a fixed window.

AuditRecordsBetween is streamed, and evidence counts its records without holding them unless the records were asked for. It stops after the period's end plus a week, and goes on while a day still holds records of the period; the one case that can miss is written into the evidence's limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The position moved only on a published commit, so a table nobody wrote to held the slot still while the rest of the database wrote, until max_slot_wal_keep_size invalidated it. With no transaction open and every subscriber's buffer empty, keepalives and XLogData's ServerWALEnd now acknowledge max(acked, position). Inside a transaction, with events still buffered, or with nobody attached, nothing moves.

Moving the slot past the last commit a client saw would have made every reconnect on a quiet table a refetch, because the resume check treats anything before the slot's confirmed position as gone. quiet.go keeps, per slot, the span acknowledged with nothing published so such a position can still resume. Without a span, after a restart, it stays a refetch: never a silent gap.

Also guards empty CopyData, which indexed Data[0] and would have panicked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SECURITY DEFINER functions in schema bb ran for anyone who could call one,
and ledger.sql granted the whole schema to db_client: bb._record_impact
wrote chained entries from caller-supplied actors.

Revoke by default: every SECURITY DEFINER function not on the list of
client-callable ones loses EXECUTE from PUBLIC and db_client, on every
install, and an event trigger seals any made later. The trigger runs ALWAYS
because most of the later schema files run in replica mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ALTER and DROP on the data guards were refused, but CREATE OR REPLACE
TRIGGER on a bb_ name was not, and the recorder skipped it by name, so
anyone holding TRIGGER on a table could silently swap a guard for a no-op.

A trigger with a bb_ name on a user's table must now match what the
Blackbox installs (tgfoid, tgtype, tgattr, tgqual), or it is refused unless
the override is in force, and the attempt is recorded BLOCKED. The engine's
own install matches and stays unrecorded. A replacement under the override
is recorded as a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
foxbyte-get Ready Ready Preview Nov 10, 2026 12:14pm UTC

@SauravYadav12
SauravYadav12 merged commit f904dac into main Nov 10, 2026
8 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant