benjivers/val Public Public: anyone can read this; the crew can change it.

Bug check on main: hunt what the tests are not catching #6

Closed opened this has it
opened this

val is merged and live at https://val--forge-apps.benjivers.com. Before anything else is built on it, go through main looking for what is actually broken. This is your job, not a feature pass: do not add capabilities, do not change how anything looks, do not rename anything a player sees.

Where the bugs are likeliest

The rules are pure functions and they are where a wrong number hides quietly:

  • The economy. 800 to start, 9,000 cap, +200 a kill, +300 to the team on a plant, loss bonus 1,900 / 2,400 / 2,900. Check the cap is applied after every award and not just some; check the loss streak resets on a win and does not run past the top bonus; check a player who joins mid-match gets a sane balance rather than a bot's leftovers.
  • The round. 100s round, 4s plant, 45s fuse, 7s defuse with the checkpoint at 3.5s. Check the checkpoint survives the defuser dying and being replaced, that a defuse completing in the same tick as the fuse expiring resolves one way and not both, and that the swap happens after 12 and the match ends at 13 — including 12–12, where there is no overtime and something has to happen.
  • Abilities. Charges bought during buy phase, signature on a cooldown, ultimate on points. Check a charge cannot be spent twice in one tick, that a cooldown started in one round does not carry oddly into the next, that ult points survive the side swap correctly, and that blindness ends.
  • The spike. Carry, drop on death, pick up. Check it cannot be planted off-site, cannot be picked up by a defender, cannot be planted twice, and that a carrier disconnecting does not take it out of the world.
  • Joining and leaving. A seat is taken from a bot when a person connects and given back when they leave. Check a disconnect mid-plant, mid-defuse and mid-buy, and check two sockets cannot end up driving the same seat.

The boundary, which is not optional

Every output escaped, every socket message bounded and validated, nothing trusted from a client that the server should decide, all state under FORGE_APP_DATA, no secret in the repository, and the security floor in src/server.ts on every response. The socket takes JSON from anyone on the internet: check what a malformed, oversized, or wrong-typed message does, and what happens if a client sends 500 messages a second.

How to work

For anything you find: fix it in the smallest way that closes it, and add a test that fails against the code as it was. Prove the test bites — break the line it covers, watch it go red, put it back. Say in the pull exactly what a player or an attacker could have done before the fix.

If you sweep it and find nothing real, say that. A clean report honestly given is a useful result; inventing findings to look busy is the failure mode of this job. Either way the pull description should list what you checked, not only what you changed.

Not bugs, and not yours to fix here: there is no client-side prediction, vision is a full circle rather than a view cone, there is no overtime at 12–12, and bots commit to a site rather than rotating on sound. Those are known and deliberate.

commented

Found one while you were working, and it is the big one. Add it to the sweep.

One client can take a seat on both teams at once, which defeats vision entirely.

I opened four websockets from a single process against the live app. Each one was handed its own seat, alternating sides:

bot-red-0/red   bot-blue-0/blue   bot-red-1/red   bot-blue-1/blue
distinct seats held simultaneously: 4

Then two sockets, one per side, compared snapshots:

socket 1 (red)  sees 5 players
socket 2 (blue) sees 5 players
union of what ONE cheater sees across two sockets: 10 of 10

So the server-side vision work — which is genuinely correct, per socket — is bypassed by joining the other team and reading its snapshot. Every enemy position, continuously, with no forged packets and nothing malformed: just two honest connections. It is also a seat-denial vector, since ten sockets take the whole match and leave real people with nothing.

The cause is that open() in src/server.ts hands out a seat per connection, and a connection is anonymous — nothing ties two sockets to the same person. Every other app on this forge identifies a visitor with one opaque HttpOnly cookie; this one does not.

What the fix needs to decide (say which you chose in the pull): a person holds at most one playing seat. The straightforward version is the cookie the other apps use — mint it on the page, read it on upgrade, and refuse a second playing seat for a session that already has one. Extra connections should still be allowed to watch (they are useful — a second tab, a phone), but a watcher must be given a spectator snapshot built for no team, not a seat with a side, or you have moved the hole rather than closed it. Whatever you choose, a test should hold two sockets for one session and assert the second gets no seat on the other side.

Two smaller things from the same probe, both non-fatal:

  • Garbage survives cleanly: bad JSON, wrong types, a 5KB message, NaN/Infinity coordinates, and a 100-message burst all left the server serving and my player sane. Good.
  • But angle is stored unvalidated — I sent a: 99999 and the snapshot came back "angle": 99996.3. Trig makes it harmless today; normalise it anyway, since it is the value the flash's facing check reads.
Sign in to join this conversation.

↑↓ navigate ↵ open esc close

Keyboard shortcuts