表单、身份与安全:隐藏按钮不是权限控制
把“发布”按钮藏起来,普通访客看起来就不能发布了。但任何人都可以跳过页面直接发请求。前端负责把操作解释清楚,最终授权必须由服务端或数据库执行。
表单验证有两层
前端及时告诉用户标题不能为空、长度超过限制,减少无效提交;服务端再次验证,因为客户端检查可以被绕过。不要只用 HTML required 就相信数据可信。保存失败要保留输入并给出具体反馈,别用刷新页面清掉用户写了半小时的草稿。
异步提交还需要 pending 状态、重复操作策略和错误归属。字段错误放到字段附近并关联说明,整体网络错误放到表单层。先聚焦到需要修正的位置,让键盘用户也知道接下来做什么。
XSS 常从“我要显示富文本”开始
Vue 插值和 React 文本渲染默认会做相应转义,但 v-html、dangerouslySetInnerHTML 和 Markdown 转 HTML 会进入不同风险区域。展示不可信 HTML 时,应通过明确白名单进行净化,过滤危险标签、属性和 URL 协议;不要靠正则表达式“去掉 script 标签”来防护。
本知识库有 Markdown 净化和安全测试,可以沿着 app/utils/markdown.ts 与 tests/security/markdown-security.test.ts 阅读真实实现。CSP 可以补充防线,但不能代替正确的输出处理。图片、链接、嵌入内容也要检查,不只是 script。
会话、CSRF 与 CORS 别混在一起
HttpOnly Cookie 减少脚本直接读取会话凭据的机会,但认证仍需考虑 CSRF;SameSite、来源校验、CSRF token 等策略取决于具体架构。把 token 放在可被脚本读取的存储里,则要清楚 XSS 可能窃取它。不存在适用于所有应用的“一行最安全存法”。
CORS 管浏览器跨源读取规则,不是服务端认证系统。攻击者的服务器不受浏览器 CORS 限制。前端看到 CORS 错误时,先查服务端响应和部署配置,不能通过开放所有来源来掩盖鉴权问题。
Supabase 的公开 Key 为什么能出现在浏览器
Publishable/Anon Key 标识项目访问入口,数据权限由 RLS 和用户身份共同控制。它公开不意味着数据应该公开可写。service role 或 secret key 有更高权限,只能在可信服务端环境使用,绝不能放入 Nuxt public 或 Vite 客户端变量。
阅读本仓库的迁移可以看到,公开读取已发布内容与管理员写入分开处理。测试时至少检查匿名读草稿、普通用户写文章、跨用户访问等场景,不能只证明管理员登录后能操作。
练习与参考
在测试环境向 Markdown 输入事件属性、javascript 链接和恶意 HTML,确认都不能执行。再用匿名请求尝试写入,预期由权限层拒绝,而不是依靠页面没有按钮。
按数据经过的边界检查一次标题保存
输入框中的字符串是客户端数据;请求到达服务端仍是不可信输入;通过身份认证只说明请求代表某个身份,还没有证明这个身份能改这篇文章。服务端要再验证资源归属或角色权限,并在数据库查询条件中约束目标范围。
例如用户 A 把请求中的 articleId 换成用户 B 的文章 ID,服务器应拒绝。页面上没有 B 的编辑按钮不能证明这个攻击失败。请求里的 role=admin 也不能当作权限来源,权限应来自服务器可信的会话或数据库规则。
写入成功之后,显示标题时继续按文本处理。数据来自自己的数据库不代表天然可信,因为它可能最初由用户、导入脚本或被攻破的接口写入。富文本净化要放在明确的呈现边界,且注意净化之后再次拼接或修改 HTML 可能引入新风险。
CSRF 与 XSS 需要分别验证:前者关心浏览器是否会带着受害者凭据发起非预期请求,后者关心攻击内容是否成为页面执行代码。HttpOnly 能限制读取 Cookie,但页面若被注入脚本,攻击者仍可能以当前用户发起操作,因此它不是 XSS 的完整解法。
