🔒 Fix XSS in chat/killcam/scoreboard + scope quick-chat to the match - #3
Open
longmaolab wants to merge 1 commit into
Open
🔒 Fix XSS in chat/killcam/scoreboard + scope quick-chat to the match#3longmaolab wants to merge 1 commit into
longmaolab wants to merge 1 commit into
Conversation
Player-controlled text (names, chat) was interpolated straight into innerHTML, allowing XSS against other players — including persistent XSS via the localStorage kill log. Quick-chat was also broadcast globally (socket.broadcast.emit) instead of to the match. - Add escapeHtml() + safeColor() helpers to game.js (mirrors chat.js:431) - Escape chat-feed text + validate colour (renderChatFeed) - Escape killcam victim/weapon (openKillTheater) - Escape scoreboard player name (showScoreboard) - Scope chatLine to emitToMatch(p.matchId, ...) in server.js No gameplay changes. node --check passes on both files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5 tasks
Collaborator
Author
|
🔗 Follow-up issues (not blocking this PR — the "next steps" from the same friendly review, in suggested order):
Tackle them whenever — each stands alone. 加油! 🚀 |
2 tasks
VideoGameTips
added a commit
that referenced
this pull request
Aug 14, 2026
…dels ## People no longer walk like mannequins The rig had one rigid segment per limb pivoting only at the shoulder and hip, both legs driven by the same pure sine, amplitude switching between 0 and full on a single frame. That reads as a shop dummy being slid along the floor, and no amount of timing tweaks fixes it while the limbs cannot bend. - Limbs are now two segments with a joint: hip → thigh → KNEE → shin → foot, and shoulder → upper arm → ELBOW → forearm. Total lengths are unchanged (0.65 leg, 0.6 arm) so silhouettes, skins and hitboxes are untouched. Boots are separate and deliberately excluded from legLimbs so they stay dark on every skin; both new segments ARE in armLimbs/legLimbs so skin recolouring still covers the whole limb exactly as before. - Knees flex on a curve timed into the swing phase, so the shin clears the ground instead of the leg scything through it. Feet counter-rotate to keep the sole level, which is most of what makes a walk look weighted. - Elbows stay slightly bent even at rest and never lock straight. - Per-character gait, drawn once at build time: phase offset, stride length, swing amplitude, left/right asymmetry, bob and lean. Identical bots stepping in perfect unison was a louder "these are machines" tell than any missing joint; three characters now sit at genuinely different phases. - Gait curve is a sine plus a little second harmonic, because a real leg's swing is quicker than its stance. - Shoulders and hips counter-rotate against each other, the body rolls onto the loaded leg, and the head counters the torso so characters keep looking where they are going. - Body bob, twice per stride. Driven off every direct child's captured base Y rather than a wrapper group, because the skin code parents helmets, visors, ears and the crown straight onto the group — a wrapper would leave those hovering while the head moved. Base Y is captured lazily so the crown, added later, is picked up when it appears. Name tag is excluded; a jittering label is hard to read. Crouch drop is a deliberate 0.12: the legs bottom out only 0.225 above the group origin, so more puts the boots through the floor mid-slide. - Starts and stops are eased through a blend factor instead of snapping. ## Weapons - `mMat`, shared by every hand-built gun, was Lambert — which has no specular term at all, so every barrel in the game read as flat charcoal. It is Phong now, which gives a highlight to all of them from one line. - `_genericGun` (83 weapons route through it) gains an upper/lower receiver seam, a magazine well, trigger and trigger guard, charging handle, vented handguard, gas block, muzzle device, iron sights when no optic occupies the top deck, and a stock with a comb and an angled butt pad. - The core guns predate `_genericGun` and were each a handful of bare boxes. Added `_gunDetails()` for the shared furniture and wired up the seven players actually hold — AK20 (the default), AK30, SG8, MP40, P90, Pistol, SRX — plus per-gun character: the AK's wooden handguard, SG8's shell tube, MP40's perforated shroud, SRX's bipod. Explicit dimensions per caller on purpose; deriving them from a bounding box would misplace details on the umbrellas, baguettes and chainsaws that also live in this file. - `makeBotWeaponProp` — the model you see on everyone ELSE — was a box and a tube. Now has a magazine, stock and butt pad, handguard, muzzle, and a real scope for snipers; melee is a tapered shaft with a wrapped grip and weighted head; grenades have a fuse cap, spoon and ring. Verified in the browser, not just node --check: all 121 weapons still align 1:1 with weaponModels, every model still carries `_flash` (gotcha #3), all 121 bot props build without throwing, no non-finite positions anywhere in any model, and the rig drives hip/knee/twist/bob through a full stride with finite values and folds correctly on crouch. Eyeballed six weapons side by side and three characters mid-stride at different phases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VideoGameTips
added a commit
that referenced
this pull request
Aug 15, 2026
Follows d21c0c2, which covered the 83 guns routed through _genericGun and seven hand-built core guns. This does the other 31. Coverage is now accounted for across all 121 weapons: 83 via _genericGun (full detail there) 25 via _gunDetails (7 previously + 18 here) 13 already had a trigger and guard, or are thrown items reshaped by hand Conventional guns (SG100, RPD, BurstRifle, LeverRifle, AutoShotgun, VectorSMG, Revolver, Shorty, Cycler, HandCannon, MG42, Minigun) get the shared furniture, each with its own real dimensions and its own exceptions rather than a blanket pass: no charging handle on a lever action, a break-action double barrel, a sawn-off, or a revolver; no muzzle brake on rotary barrels or a sawn-off; no irons where a rail or carry handle already owns the top deck. _gunDetails grew ox/oy offsets because the MG42 is assembled off-centre and every detail would otherwise have floated to the left of the gun. Also added rearSightZ for the short weapons whose rear notch would have landed behind the receiver. Four energy weapons (Flamethrower, GrenadeLauncher, Railgun, FreezeGun) had no trigger at all. They have one now, and nothing else: their business ends are emitters, not rifled barrels, so no brake was fitted. The grenade launcher gets a ladder sight since it is the one that would actually have one. RPG and Bazooka were eight meshes each and did not read as shoulder-fired: the RPG now has a wooden heat shield, forward grip and folding ladder sight, the Bazooka a shoulder rest and firing cable. Thrown items got shape work rather than gun furniture, which would have been absurd on them: - Shuriken: four tapered blades around a hub with a hole, replacing three crossed slabs. Its material also specified `metalness`, which does nothing on a Lambert material — so it had always been flat grey. Phong now. - Boomerang: tapered two-segment arms with a rounded elbow and painted bands; two identical slabs previously read as the letter V. - ThrowingAxes: a head with an actual axe profile — flared bit, hammer poll, collar — plus a tapered haft and leather grip, replacing one brown slab. - Slingshot: two bands drawn back to a pouch instead of one straight bar. A slingshot with nothing to hold the stone is not a slingshot. - Blowgun: cord bindings and a nocked dart. - CreamPie: a ring of cream peaks, a swirl, and a stalk on the cherry. - TrafficCone: second reflective band, bevelled lip, scuffed underside. Left alone deliberately: Paintball, Crossbow, Flare and ThrowingKnives already have triggers and guards and would have got doubled geometry; Boombow is a bow; Taser is a stun unit with fifteen meshes already. Verified in the browser: all 121 models build, align 1:1 with WEAPONS, carry `_flash` (gotcha #3), have non-empty children, and contain no non-finite positions; all 121 bot props build. Eyeballed revolver, MG42, RPG, shuriken, cream pie and throwing axe side by side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VideoGameTips
added a commit
that referenced
this pull request
Aug 16, 2026
…tored Three strands land together because they had become entangled in one file. ## Handcrafted models (equipment-models.js) New registry of individually built models — 83 ranged, 23 melee, 17 utility — replacing every generic-factory placeholder, wired in via handcraftedWeapon() and loaded before game.js. Verified at runtime rather than by syntax alone: rosters align exactly at 121/121 ranged, 42/42 melee, 55/55 utility; no null, empty or non-finite models; every ranged model still carries `_flash` (gotcha #3 — firing throws without it). The reported missing `window.HandcraftedModels` was stale browser cache. The file serves fine (200, byte-exact) and the script order is correct, so no cache-busting query was needed. Performance: no optimisation warranted. Building all 123 handcrafted items costs 38 ms once (0.31 ms/item), only ONE model with ~20 meshes is ever visible, and frames sit at 16.7 ms median / 17.5 p95. ## Match isolation (the "strangers in my match" bug) enterMatch changed matchId silently, telling nobody. So a client kept every body it had already been told about — the lobby crowd walked into your match with you and stood frozen there, since position updates ARE match-scoped and never reached them again. Three holes, all closed: - movePlayerToMatch() now announces the move both ways: the match being left loses sight of the player and their bots, the match being joined gains them. - matchRoster hands the mover an authoritative list of who is actually present; the client drops everyone not on it, guarding client-only entities (training dummies, locally simulated bots). - leaveMatch was never emitted by anything. endMatch sends it now, so finishing a match returns you to the lobby instead of parking you in a dead one until you close the tab. Proven with three simulated socket clients against both builds: 12 assertions fail on the previous server, all 14 pass now. ## Dead code Removed 100 declarations (-332 lines): 85 orphaned builders left behind by the handcrafted registry, buildSimpleMelee/buildSimpleSupport (zero callers), and the universal polish block (polishWeapon/Melee/SupportModel, detailBox and its materials — each had exactly one reference, its own definition). KEPT, contrary to the cleanup list, because neither is actually dead: - `_genericGun` — openKillTheater still builds the killer's in-hand weapon with it. Deleting it would have broken the Kill Log theater. - `_gunDetails` — 25 surviving bespoke builders call it. ## Movement work restored game.js on disk had rolled back to a pre-overhaul copy, losing work already on main: applyBlastImpulse, heldItem() (so the air dash threw "not a function" again), the _extVel integration, and the gravity tuning. Restored from HEAD and re-verified live: GRAVITY 21 / JUMP_VEL 13.8 -> 4.53 m apex, blast underfoot 8.48 m, throwing knives dash. Carries in-progress work from another contributor that was already in the file (BULLET_SPEED_MULTIPLIER, the updateBullets removeBullet refactor). Left untouched and unstaged: the Vietnam map pool in server.js, and .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VideoGameTips
added a commit
that referenced
this pull request
Sep 3, 2026
Measured before touching anything: 61 of 99 guns had geometry that never touched the body — 264 parts hanging in mid-air — and only 29.6% of gun materials had any specular term at all. Both are systematic faults of hand-placing parts by eye across two thousand lines, so both are fixed in one finishing pass rather than gun by gun. weldModelParts treats each part's bounding box as a graph node and "touching" as an edge. Anything not in the same connected component as the receiver is loose, and gets nudged the shortest distance on each axis that makes it touch. Whole clusters move together, so a scope keeps its lens and a bipod keeps both legs. It iterates, because attaching one cluster can bring others into contact. shinifyModel swaps MeshLambertMaterial — which has NO specular term, so those parts could never catch light whatever colour they were — for Phong, with shininess scaled by the colour's luminance: dark polymer stays fairly matte, bright metal gets a hard highlight. MeshBasicMaterial is left alone on purpose, since glows, reticles and muzzle flashes are meant to look self-lit rather than polished. Conversions are memoised per source material, so the ~1770 meshes share a small set of new ones. Both run once at load over weapons, melee and utilities. Result: floating parts 264 -> 0 across all 99 weapons, and 0 across the 97 melee and utility models too. Shiny share 29.6% -> 99.5% (the remaining handful are transparent parts, deliberately skipped). Every weapon still carries _flash (gotcha #3), and the worst previous offenders — gau19, m134, barrett, sticker_blaster, sawed_off — were eyeballed to confirm welding tightened them without distorting the silhouettes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VideoGameTips
added a commit
that referenced
this pull request
Sep 7, 2026
Andy called it: the pieces looked like they were flying around, or like a square had been slapped on the side. They were. Measured against a classic receiver (58 mm tall), every attachment on the top deck was hovering in clear air: front sight post 7.0 mm above the handguard rear sight ears 4.0 mm above the receiver top rail 3.0 mm above the receiver red dot / holo 3-4 mm above the rail sniper scope tangent to the rail - a hairline seam pan magazine 22 mm above the receiver (a third of the body height) classic stock zero overlap with the receiver ejection port 15 mm proud of the flank - the slapped-on square Three changes, applied in _genericGun (~80 weapons) and _gunDetails (the hand-built core guns): 1. SEAT. Every part is now sunk 3 mm into whatever it mounts to instead of balanced on top of it. A part that merely touches shows background at the joint and reads as floating; sunk slightly, the volumes merge. 2. Connective geometry, because two boxes meeting is not a joint. Sights grow out of base blocks. Optics are clamped by real mounting rings and posts. A trunnion collar covers the receiver-to-handguard step. A wrist tang tapers the receiver into the stock. The charging handle rides in a milled raceway. The magwell gets a flared lip. Suppressors get a thread shoulder. The ejection port became a shallow surround with a recess behind it, proud by 1 mm instead of 15. 3. _softBox: a chamfered box helper. A hard 90 degree corner takes nearly the same shade on both faces under one light, so a pile of boxes reads as one flat grey mass. A small bevel catches a highlight along every edge, which is what separates the forms and gives the machined look. Used for all the major volumes. Also fixed pistols, whose magazine hung under the middle of the frame as its own slab - pistols get no magwell, so it entered nothing. It now sits inside the grip on the grip's angle with the baseplate proud at the heel. Verified in-page: all 3000 shape/scope/mag/stock permutations build with no errors and all keep _flash (gotcha #3). ~43 meshes per gun. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VideoGameTips
added a commit
that referenced
this pull request
Sep 7, 2026
Andy's note after looking at Rivals: the weapons there are not complicated,
they are consistent. Nothing changes length, nothing floats, and there are
no extra parts that do not mean anything. Three real bugs turned up.
1. THE GATLING GUN CHANGED SHAPE AS IT SPUN. The three barrels were not on
a common circle - barrel 1 at radius 0.0220, barrels 2 and 3 at 0.0246,
11.8% further out, spaced 116.6/126.8/116.6 instead of 120 apart. So the
silhouette wobbled while firing. Positions are generated from the angle
now, so they cannot drift again.
2. NOTHING SHARED A SCALE. Every detail in _genericGun was a hardcoded
absolute size while the bodies vary a lot, so the SAME grip came out at
74% of the receiver width on a pistol and 37% on an LMG. Details now
scale with the body through two factors (0.6 exponent, because a grip is
sized by a hand as much as by the gun). Roster-wide proportion swing:
grip 2.00x -> 1.31x magazine 1.94x -> 1.31x
rail 1.97x -> 1.31x sights 1.67x -> 1.19x
stock 1.98x -> 1.00x trigger 1.58x -> 1.20x
Rail slot count now follows rail length so the pitch stays even.
3. PARTS THAT DO NOT BELONG. A pistol works a slide, not a charging handle
on the flank; an energy weapon has no cartridge to chamber and no gas to
tap. Both are gated by action type now.
Melee and utilities, same principle:
BAT - was tapered backwards. rotation.x = +PI/2 maps the cylinder's +Y end
to +Z, so the wide 0.035 end sat at the hand and the 0.015 end was the
striking tip. It was also three meshes in two colours, so the joints read
as steps in what is really one unbroken curve. Now a single lathed
surface, widest at the tip, narrowing all the way to the knob, no seams
and nothing bolted on.
FRAG GRENADE - the four segmented ridges were tori of outer radius 0.036
inside a sphere of radius 0.045, buried 9 mm under the surface. Not one
was ever visible: four meshes of pure cost, warping around inside the
body. Deleted. It has what a grenade actually needs instead - fuse neck
and cap, a safety lever running down the side, and the ring you pull,
each overlapping what it attaches to. Body sphere went to 14x10 because
on a coarse sphere the flat of a facet sits 1.7 mm inside the nominal
radius, enough for the lever to poke back out low down.
Verified in-page: all 196 models build with no errors (99 weapons, 42
melee, 55 support) and all 99 weapons keep _flash (gotcha #3).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
TL;DR (English)
First off — this project is genuinely impressive. A solo-built browser FPS with ~117 weapons, 39 maps, bots, a killcam theater, a full shop economy, and an AI chat? That's a huge amount of working software. The kind of thing a lot of grown-up dev teams never finish.
This PR is a small, focused safety fix. Right now, a player's name and chat text are inserted straight into the page's HTML. That means another player could type something clever into their name/chat that isn't just text — it could be code that runs inside your browser. That's called an XSS bug (Cross-Site Scripting). We fix it in 3 spots with a tiny
escapeHtml()helper (one already exists inchat.js:431), lock down the chat color to a safe pattern, and stop quick-chat from leaking across matches.It's intentionally small and easy to review — no behavior changes for normal players, just safer handling of text that comes from other people.
What this PR changes
escapeHtml()topublic/game.js(mirroring the one inpublic/chat.js:431) so we have one helper to clean text before it touchesinnerHTML.public/game.js:1774-1777(renderChatFeed): escapeline.textbefore it goes intoinnerHTML.public/game.js:12578-12582(chatLinesocket handler): only accept a safe color (e.g.#fff/#ffcc66hex or a short allow-list), fall back to#fffotherwise. This stops CSS being injected through thecolorvalue.public/game.js:8953-8955(kill-log theater overlay): escapereplay.victimandreplay.weaponbeforeinnerHTML.public/game.js:12503: escapep.namebeforeinnerHTML(or build the row withtextContent).server.js:1019: swapsocket.broadcast.emit('chatLine', …)foremitToMatch(p.matchId, 'chatLine', …), matching howbulletFiredalready works atserver.js:1042.Why it matters (plain language)
When you do
element.innerHTML = "Hello " + someName, the browser doesn't treatsomeNameas just letters — it treats it as HTML. So if another player sets their name to something like<img src=x onerror=...>, that code runs in your browser, not theirs. With chat and the scoreboard, an attacker doesn't even need you to click anything — just seeing their name or message is enough to trigger it. The killcam one is sneaky: the bad name gets saved into your kill log (localStorage,pvp_kill_log) and fires every time you open the theater later.The server currently only trims these strings to a max length (
server.js:968for names,server.js:1019-1024for chat). Length-trimming is not the same as sanitizing —"><img src=x onerror=alert(1)>is short enough to slip right through. The real fix is on the client, at the moment we build the HTML.The fix approach
Two safe patterns, both fine:
escapeHtml()so<,>,&,"become harmless entities, then drop it into the template literal. Best when we want to keep the surrounding markup/styles.textContent— for plain text like the scoreboard cells, setcell.textContent = p.nameso the browser never parses it as HTML at all.For the color, escaping isn't enough on its own (it's used inside a CSS
style="..."), so we validate it against a pattern like/^#[0-9a-fA-F]{3,8}$/and fall back to#fffif it doesn't match.For quick-chat scoping,
emitToMatchkeeps each match in its own bubble (same fix that solved the old cross-match bullet/6v6 issues). The sender's own echo is still filtered client-side by the existingif (data.id === myId) return;check atgame.js:12580, so nothing breaks for the sender.Confirmed locations
innerHTMLpublic/game.js:1774-1777line.text,line.colorpublic/game.js:12578-12582data.text,data.color(from other players)innerHTMLpublic/game.js:8953-8955replay.victim,replay.weaponinnerHTMLpublic/game.js:12503p.nameserver.js:1019socket.broadcast.emit→ should beemitToMatchpublic/chat.js:431escapeHtml()reference implOut of scope (gentle note for later)
Per
CLAUDE.md, passwords are stored in plaintext on purpose — that's a documented tradeoff for a hobby/learning project, and totally reasonable while you're building and experimenting. The reason it's worth flagging here: XSS + plaintext passwords is a spicy combo. If someone can run code in your browser, and there's a plaintext password sitting inlocalStorage(pvp_user, set atgame.js:17542), they could potentially grab it. Fixing the XSS in this PR already removes the dangerous half of that combo, so you're in good shape. Hashing passwords (bcrypt/argon2) is a great "someday, if this goes production" upgrade — not something this PR needs to touch.中文详细说明
先说一句:这个项目真的很牛。一个人独立做出来的浏览器 FPS,约 117 把武器、39 张地图、机器人 AI、击杀回放剧场(killcam theater)、完整的商店经济系统,还有 AI 聊天 —— 这个工作量非常大,很多成年人的开发团队都未必能做完。为你点赞 👏
这个 PR 是一个小而专注的安全修复,对正常玩家没有任何体验上的改动,只是更安全地处理「来自其他玩家的文字」。
这是什么问题?(XSS / 跨站脚本)
游戏里有几个地方,是这样往页面里塞内容的:
问题在于:浏览器不会把「别人的名字」当成纯文字,而是当成 HTML 代码来解析。所以如果有个玩家把自己的名字改成类似
<img src=x onerror=...>这样的东西,这段代码就会在你的浏览器里运行,而不是在他的浏览器里。这就叫 XSS(跨站脚本攻击)。更麻烦的是:聊天和记分板这两个地方,攻击者根本不需要你点任何东西 —— 你只要「看到」他的名字或消息,代码就触发了。而击杀回放那个更阴险:坏名字会被存进你的击杀日志(
localStorage里的pvp_kill_log),以后你每次打开剧场都会再触发一次。服务器现在只是把这些字符串截断到最大长度(名字在
server.js:968,聊天在server.js:1019-1024)。但「截断长度」不等于「消毒」—— 像"><img src=x onerror=alert(1)>这种攻击代码很短,照样能溜进来。真正该修的地方,是在客户端「拼 HTML 的那一刻」。这个 PR 改了哪些地方
public/game.js加一个escapeHtml()帮助函数(照搬public/chat.js:431里已经有的那个),统一用它在文字进入innerHTML之前清理一遍。public/game.js:1774-1777(renderChatFeed函数):line.text进innerHTML之前先转义。public/game.js:12578-12582(chatLinesocket 处理器):颜色只接受安全格式(比如#fff这种十六进制,或一个小白名单),不符合就回退到#fff。这样能挡住通过color注入的 CSS。public/game.js:8953-8955(killcam 剧场覆盖层):replay.victim和replay.weapon进innerHTML之前先转义。public/game.js:12503:p.name转义后再用(或者干脆用textContent来建这一行)。server.js:1019:把socket.broadcast.emit('chatLine', …)换成emitToMatch(p.matchId, 'chatLine', …),跟server.js:1042那里bulletFired已经在用的写法保持一致。修复思路
两种安全写法,都可以:
escapeHtml(),把<、>、&、"变成无害的实体字符,然后再放进模板字符串。想保留外面那层样式/标签时用这个。textContent—— 像记分板那种纯文字格子,直接cell.textContent = p.name,浏览器压根就不会把它当 HTML 解析。颜色那个光转义还不够(它是放在 CSS 的
style="..."里面用的),所以要用类似/^#[0-9a-fA-F]{3,8}$/的正则去校验,不匹配就回退成#fff。快捷聊天的范围问题:
emitToMatch能让每一局比赛各自独立(就是当年修好「子弹串场 / 陌生人一进来就变 6v6」那个老 bug 的同一招)。发送者自己的回声,仍然由game.js:12580那行已有的if (data.id === myId) return;在客户端过滤掉,所以对发送者来说不会有任何破坏。已确认的位置(都来自核实过的证据)
innerHTMLpublic/game.js:1774-1777line.text、line.colorpublic/game.js:12578-12582data.text、data.color(来自其他玩家)innerHTMLpublic/game.js:8953-8955replay.victim、replay.weaponinnerHTMLpublic/game.js:12503p.nameserver.js:1019socket.broadcast.emit→ 应改为emitToMatchpublic/chat.js:431escapeHtml()实现暂不处理(温和地留个「以后再说」)
根据
CLAUDE.md,密码是故意明文存储的 —— 这是你为这个业余/学习项目做的、有记录在案的取舍,在你边做边玩的阶段完全合理。之所以在这里提一句,是因为:XSS + 明文密码 是个有点危险的组合。如果有人能在你浏览器里跑代码,而localStorage里又躺着一个明文密码(pvp_user,在game.js:17542写入),他就有可能把它偷走。这个 PR 修好 XSS,其实已经把这个组合里最危险的那一半干掉了,所以你已经稳了。给密码加哈希(bcrypt/argon2)是个很棒的「将来上线了再说」的升级 —— 这个 PR 不需要碰它。这个 PR 故意做得小、好审、好合。你已经把最难的部分(整个游戏)做出来了,现在养成「来自别人的文字,先消毒再上屏」这个习惯,就是从「能跑的游戏」走向「专业级代码」的一小步。继续加油,做得真的很棒!🚀