Skip to content

🔐 Security hardening for "someday": hash passwords + stop sending password in the URL #8

Description

@longmaolab

Suggested labels: security, enhancement

TL;DR (English)

You already did the urgent thing — PR #3 closes the XSS holes, which were the scary "another player runs code in your browser" bugs. 👏 This issue is the calmer, someday layer of security: two hardening steps that are totally fine to leave as-is while you're learning, but worth writing down so they're not forgotten if this ever goes public/serious.

Per CLAUDE.md, both of these are documented, intentional hobby tradeoffs — so this isn't "you did it wrong," it's "here's the graduation path." Two items:

  • (1) Hash passwords instead of storing them in plaintext. Today they're stored & compared as-is (server.js:362 compare, server.js:562 store) and saved to localStorage on the client (public/game.js:17542). Node has a built-in hasher (crypto.scrypt) — no npm install needed.
  • (2) Stop sending the password in the URL. GET /shop/inventory reads ?username=…&password=… from the query string (server.js:342-345). URLs get written to server logs, proxy logs, and browser history. Switch it to POST with the password in the request body — exactly like /shop/buy already does.

Neither is urgent for a hobby game. Filing so it's on the map. 🗺️


中文详细说明

先说一句:紧急的那部分你已经搞定了 —— PR #3 把 XSS 堵上了,那才是"别人能在你浏览器里跑代码"的可怕 bug。👏 这个 issue 是更从容的、"以后再说"的安全加固。这两件事在你学习/玩耍阶段保持现状完全 OK,只是写下来,免得将来真要公开/正式上线时忘了。

CLAUDE.md 里写明了:这两条都是你有意做的、有记录的业余取舍。所以这不是"你做错了",而是"毕业路线图"。

(1) 把密码哈希,而不是明文存

现在密码是明文存和比对的:

  • server.js:362 —— if (!u || u.password !== password)(明文比对)
  • server.js:562 —— 注册时直接 password 存进 users.json
  • public/game.js:17542 —— 客户端把明文密码写进 localStorage

为什么要改: 万一 users.json 泄露(或有人翻到 localStorage),所有人的密码就直接暴露了 —— 而且很多人到处用同一个密码。哈希之后,存的是"加盐后的乱码",就算泄露也还原不出原密码。

怎么改(用 Node 自带的 crypto,不用装任何东西):

const crypto = require('crypto');
// 注册时:
function hashPassword(pw) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.scryptSync(pw, salt, 64).toString('hex');
  return `${salt}:${hash}`;            // 存这个字符串,而不是明文
}
// 登录时:
function checkPassword(pw, stored) {
  const [salt, hash] = stored.split(':');
  const test = crypto.scryptSync(pw, salt, 64).toString('hex');
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(test));
}

💡 客户端那边,登录后别再存明文密码localStorage —— 存一个登录后服务器发的"令牌"(token)更安全。这步可以等哈希做完再说。

(2) /shop/inventory 别把密码放进网址

server.js:342-345:

app.get('/shop/inventory', (req, res) => {
  const username = req.query.username;
  const password = req.query.password;   // ← 密码出现在 URL 里

为什么要改: 网址会被写进服务器日志、Cloudflare/代理日志、甚至浏览器历史记录。密码躺在网址里 = 到处留底。

怎么改: 把它从 app.get 改成 app.post,密码从 req.body 取(跟 /shop/buy 一模一样),客户端那边对应的请求也改成 POST + body。


🔗 相关 / Related: PR #3 已经把"XSS + 明文密码"这个危险组合里最危险的一半(XSS)干掉了。这个 issue 是把另一半也补上 —— 等将来真要上线,你就两头都稳了。不急,慢慢来。做得很棒!🔐

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions