Skip to content

test: cover the untested auth paths — and fix the three bugs that found - #49

Merged
FMSMITH91 merged 2 commits into
mainfrom
test/coverage-gaps-and-three-bugs
Aug 8, 2026
Merged

test: cover the untested auth paths — and fix the three bugs that found#49
FMSMITH91 merged 2 commits into
mainfrom
test/coverage-gaps-and-three-bugs

Conversation

@FMSMITH91

Copy link
Copy Markdown
Owner

I measured coverage first rather than guessing. 50% overall, and the gaps weren't where you'd assume: app.py at 42% is mostly install/bootstrap work that needs a real host. What stood out were small, fully-testable functions that decide who may do what, with zero executed lines.

Writing tests for them turned up three real bugs.

Bug 1 — the file manager could delete the game install

_is_protected_path is the only guard between the delete button and an unrecoverable wipe. It read the raw path string, while delete_path runs rm -rf on the resolved one. Anything that normalised onto a protected tree walked straight through:

you type what rm -rf actually gets
./lgsm /home/<user>/lgsm — the LinuxGSM control tree
x/../serverfiles /home/<user>/serverfiles — the entire game install
a/b/../../.ssh /home/<user>/.ssh — the host's SSH keys

Six forms in total, all now closed by normalising inside the guard, so both callers are covered and anything climbing out of the home dir collapses to "refused". Ordinary paths stay deletable.

Bug 2 — a corrupt host key showed a confident fingerprint

base64.b64decode's lenient default discards characters outside the alphabet, so a pin like "ssh-rsa ***" decoded to b"" and printed the SHA-256 of nothing — a real-looking fingerprint for a key that isn't there, in the one place an operator is asked to eyeball what they're trusting. validate=True now.

Bug 3 — a player-authored escape byte reached the page

strip_escapes dropped sequences matching its three grammars, but an ESC starting a sequence that matched none of them survived: a trailing \x1b, or \x1b\t / \x1b\x00. Console text carries player names and chat, so those bytes are authored, not accidental. Found by the new fuzz target's property, not by hand.

The tests

Every one verified by mutation.

  • API tokens (unit + smoke) — a Bearer token authenticates as its owner, inherits their full RBAC, and app.py exempts Bearer requests from CSRF. An entire authentication path with no test. Now asserts only the hash is stored, that replaying the stored hash does not authenticate (the property that makes hashing worth anything), that a token sees only its owner's servers, and that deactivating or revoking the owner kills it.
  • can_run_custom_command (smoke) — eight branches deciding who may run a superadmin-authored console command. The one that matters: access to the server is not access to the command.
  • Privilege escalation (rbac) — a delegated MANAGE_GROUPS admin can edit a group they belong to, so _grantable_perms is all that stops self-promotion. The existing "can't grant super_admin" check passes even with the guard deleted, because super_admin is filtered separately. Now asserts a real permission can't be granted, and that an edit preserves one the editor can't grant.
  • Bulk actions (rbac) — /api/servers/bulk-action isn't an <int:server_id> route, so the structural sweep never saw it. Asserts a server on a non-granted host is refused and that no SSH command is issued. Under mutation this one caught the real command that would have fired.
  • The file-manager guard (unit) — including delete_path end-to-end, asserting no shell command runs at all for a protected path.
  • A fifth fuzz target, consoleterminal.py is the only parser whose input is partly attacker-authored. Asserts no raise, no ESC and no CR survive. 12 seeds, wired into the fuzz matrix.

Also: .gitattributes now keeps corpora byte-exact — * text=auto eol=lf had rewritten CRLF out of two console seeds on the way into the index, deleting the exact bytes they exist to test.

unit 763 → 821 · smoke 253 → 268 · rbac 60 → 65, lint clean.

🤖 Generated with Claude Code

FMSMITH91 and others added 2 commits August 8, 2026 06:02
Measured line coverage first rather than guessing: 50% overall, and the gaps
were not where you'd assume. app.py 42% is mostly install/bootstrap work that
needs a real host. What stood out instead were small, fully-testable functions
that decide who may do what, with ZERO executed lines.

Three real bugs, each found by a test written for something else:

1. ssh_manager._is_protected_path — the file manager's only guard against
   deleting the game install. It read the RAW path string while delete_path
   `rm -rf`s the RESOLVED one, so anything that normalised onto a protected
   tree walked straight through:

       ./lgsm            -> rm -rf /home/<user>/lgsm         (LinuxGSM tree)
       x/../serverfiles  -> rm -rf /home/<user>/serverfiles  (the game install)
       a/b/../../.ssh    -> rm -rf /home/<user>/.ssh         (the host's keys)

   Six forms in total. Now normalised inside the guard, so both callers are
   covered and anything climbing out of the home dir collapses to "refused".

2. models.host_key_fingerprint — base64.b64decode's lenient default DISCARDS
   characters outside the alphabet, so a corrupted pin like "ssh-rsa ***"
   decoded to b"" and printed the sha256 of nothing: a confident-looking
   fingerprint for a key that isn't there, in the one place an operator is
   asked to eyeball what they're trusting. validate=True now.

3. terminal.strip_escapes — an ESC that starts a sequence matching none of the
   three grammars survived to the page: a trailing "\x1b", or "\x1b\t" /
   "\x1b\x00". Console text carries player names and chat, so those bytes are
   authored, not accidental.

New tests, all verified by mutation:

- API tokens (unit + smoke). A Bearer token authenticates as its owner and
  inherits their full RBAC, and app.py exempts Bearer requests from CSRF —
  a whole authentication path with no test. Asserts only the hash is stored,
  that REPLAYING the stored hash does not authenticate, that a token sees only
  its owner's servers, and that deactivating or revoking kills it.
- can_run_custom_command (smoke). Eight branches deciding who may run a
  superadmin-authored console command; the one that matters is that access to
  the server is not access to the command.
- Privilege escalation (rbac). A delegated MANAGE_GROUPS admin can edit a group
  they belong to; _grantable_perms is all that stops self-promotion. The
  existing "can't grant super_admin" check passes even with the guard deleted,
  because super_admin is filtered separately. Now asserts a real permission
  cannot be granted, and that an edit preserves one the editor cannot grant.
- Bulk actions (rbac). /api/servers/bulk-action is not an <int:server_id>
  route, so the structural sweep never saw it. Asserts a server on a
  non-granted host is refused AND that no SSH command is issued.
- The file-manager guard (unit), including delete_path end-to-end asserting no
  shell command runs at all for a protected path.
- A fifth fuzz target, console, over terminal.py — the only parser whose input
  is partly attacker-AUTHORED. It asserts no raise, no ESC and no CR survive;
  the ESC property is what found bug 3. 12 seeds, wired into the fuzz matrix.

unit 763 -> 821, smoke 253 -> 268, rbac 60 -> 65.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`* text=auto eol=lf` rewrote CRLF out of two console seeds on the way into the
index, so crlf_log and jline_echo were committed as LF — deleting the exact
byte sequence they exist to feed the renderer (a CRLF log line, and JLine's
in-place echo ending in CRLF).

Corpora are binary inputs, not text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · 0 duplication

Metric Results
Complexity 0
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@FMSMITH91
FMSMITH91 merged commit 70533a1 into main Aug 8, 2026
20 of 21 checks passed
@FMSMITH91
FMSMITH91 deleted the test/coverage-gaps-and-three-bugs branch August 8, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant