☰
Web安全基础知识:按请求链路构建攻击面与防御体系
2026/11/3 16:30:02 网站建设 项目流程

Web安全基础知识这几个字,我前后整理过三版笔记。第一版是刚入行时抄的漏洞名词表,SQL注入、XSS、CSRF、文件上传,一条条背下来,考完就忘;第二版是在某公司做内部系统上线自查时被逼出来的,扫描器报了两百多条,真真假假混在一起,我花了两周才把误报筛干净;第三版才算是想明白了——Web安全基础知识不是一份漏洞清单,而是一套“数据在哪、谁能碰、碰完变成什么”的追踪方法。这套方法一旦立起来,新出的漏洞类型往上一挂就能对上号,不用每次从零学。

这篇内容适合谁看:写过接口但没系统做过安全自查的后端、接过登录和支付但说不清 Cookie 属性的前端、要负责上线评审的技术负责人,以及刚开始接触渗透测试、想搞懂“为什么这么防”的同学。我会按照自己整理笔记的顺序来讲,从整体地图到具体关卡,每一段都尽量给出可落地的做法和踩过的坑,不讲空泛的原则。

1. 先把攻击面摸清楚:Web安全知识地图怎么搭

1.1 别从漏洞名单开始背,从一条请求的路走一遍

我见过太多人的安全笔记是从“OWASP Top 10”开始抄的,抄完放在收藏夹里吃灰。问题在于,漏洞名单是结果,不是过程。真正好用的一版,是我按“一次请求的完整旅程”画出来的:浏览器发起请求 → 网关/反向代理 → 应用路由 → 业务逻辑 → 数据访问层 → 数据库或缓存 → 响应回到浏览器渲染。沿途每一个箭头都是一次数据形态转换,每一次转换只要有“外部数据被当成指令”的可能,就是一个攻击面。

这个视角的好处是它天然带着排查顺序。比如一个搜索接口,数据从 URL 参数进来,进到 SQL 语句,再原样回到页面 DOM 里。这条链上就有两个关键点:进数据库那次转换(注入风险),回页面那次转换(XSS 风险)。你在笔记上画一条线,标两个点,比背十条漏洞定义有用得多。

我在整理时会给每条链路标注三样东西:数据来源(用户输入、第三方回调、内部服务、定时任务)、数据落点(SQL、模板、文件路径、命令、响应体、日志),转换方式(拼接、序列化、转义、截断)。这三样写清楚了,防御手段基本就是唯一解,不需要记太多东西。

1.2 记住三条边界线,比记一百个漏洞名管用

所有 Web 安全问题,我最后都归到三条边界上。只要边界上有一道明确的、集中的检查,绝大部分问题不会漏。

输入边界——外部数据第一次进入系统的地方。注意“外部”不只有普通用户,还包括第三方接口回调、消息队列里其他团队投递的消息、运维导入的 Excel、爬虫抓回来的内容。很多人只防了表单,忘了导入和回调,这是实际项目里最常出事的口子。

身份边界——从匿名到登录、从普通用户到管理员、从 A 租户到 B 租户的每一次身份切换。这里的核心不是“有没有登录”,而是“每次操作有没有重新确认这个身份有权限碰这条数据”。我见过的越权问题,九成不是没做登录校验,而是校验了登录、没校验归属。

信任边界——服务之间调用、内外网之间的转发、浏览器与服务端的信任差异。浏览器传来的任何东西都不可信,包括藏在请求头里的、藏在 Cookie 里的、甚至是前端做过校验之后再传过来的。前端校验是体验,服务端校验才是安全,这句话我写在了笔记第一页。

1.3 把笔记落成一张能照着做的对照表

光有概念不行,我把它压缩成一张表,每次新项目启动时对着过一遍,能覆盖八成常见问题。

层级关注点典型问题自检动作
接入层传输与头部明文传输、缺少安全响应头全站强制 HTTPS,补HSTS、nosniff、X-Frame-Options
应用层输入与输出注入、XSS、路径穿越所有入口走统一校验,所有输出按上下文编码
业务层身份与权限水平/垂直越权、重复提交每个接口显式声明所需权限,校验数据归属
数据层存储与备份明文密码、敏感字段裸存慢哈希存密码,手机号身份证等加密或脱敏
运维层配置与依赖默认口令、过期组件、调试接口暴露上线前扫依赖、关调试开关、改默认端口与口令

这张表我一般是打印出来贴在工位上,新项目立项的时候对着填,填不出来的地方就是需要补课的地方。

2. 输入这关:注入类问题的根子和堵法

2.1 SQL 注入:为什么参数化查询是唯一正解

SQL 注入的原理说穿了很简单:用户输入被拼进 SQL 字符串,数据库把这段输入当成了语句结构的一部分。关键词是“结构”。只要输入能改变语句结构,不管你前面加了多少过滤器,都是在跟解析器的语法规则赛跑,早晚会输。

参数化查询之所以是唯一解,不是因为它“过滤得干净”,而是因为它从根本上改变了数据的传递方式:SQL 语句的结构先被数据库预编译固定下来,用户输入只能作为“值”填进占位符,无论里面写了什么,都不会被当作语法解析。这是机制层面的隔离,不是字符串层面的清洗。

# 错误示范:字符串拼接,输入直接进入语句结构 sql = "SELECT id, name FROM users WHERE name = '" + name + "'" cursor.execute(sql) # 正确写法:参数化查询,结构与数据分离 sql = "SELECT id, name FROM users WHERE name = %s" cursor.execute(sql, (name,))

这里有两个实际操作中的坑,值得单独说。

第一个坑是LIKE 查询和排序字段。占位符只能替代“值”,不能替代表名、列名、排序方向。很多人图省事,把ORDER BY后面的字段名直接拼接,这就是典型的注入点。我的做法是维护一份白名单字典,前端传sort=create_time,后端映射成真实列名,映射不到就用默认值。

第二个坑是批量操作和IN查询。有人为了省事用字符串拼出IN (1,2,3),正确做法是按参数个数动态生成占位符:

ids = [101, 102, 103] placeholders = ",".join(["%s"] * len(ids)) sql = f"SELECT id, title FROM articles WHERE id IN ({placeholders})" cursor.execute(sql, ids)

注意f-string里只拼了占位符本身,没有任何外部数据进去,这才是安全的用法。我看到 f-string 就紧张,但只要确认拼接的内容完全来自代码常量或占位符数量,就没问题。

实操心得:ORM 不等于安全。ORM 的原生查询方法(各种raw、execute、whereRaw)依然会原样执行字符串。团队里我一般直接定规矩——原生查询必须走代码评审,评审时只看一件事:拼接进语句的部分,是否全部来自常量。

2.2 命令执行、模板渲染、路径拼接:收敛到白名单

注入其实是一类问题的统称,SQL 只是最常见的一种。系统命令、模板表达式、文件路径、LDAP 查询、日志格式串,只要一个“解释器”存在,就有同样的风险。

防御思路可以统一成一句话:不要让用户输入参与“结构决定”,只让它参与“值选择”。落到操作上就是白名单。命令执行不允许拼接,只能从预定义的命令列表里选;模板不允许用户传模板内容,只能传变量;文件路径不允许用户传完整路径,只能传文件名,且服务端把文件名拼到固定的根目录下,再校验拼接后的绝对路径是否还在根目录内。

import os BASE_DIR = "/data/uploads" def safe_path(filename: str) -> str: # 只取最后一段,去掉任何目录成分 name = os.path.basename(filename) full = os.path.realpath(os.path.join(BASE_DIR, name)) # 关键一步:解析后必须仍在基目录内 if not full.startswith(os.path.realpath(BASE_DIR) + os.sep): raise ValueError("非法路径") return full

realpath这一步非常关键,因为它会解析掉../和软链接。只用startswith判断拼接前的字符串是挡不住的,/data/uploads/../../etc/passwd这种输入在字符串层面看起来是以基目录开头,解析完就跑到外面去了。这个小细节我在第一次做文件下载功能时踩过,当时以为做了前缀校验就万无一失,后来被同事一句话点醒。

2.3 校验分三层:语法、语义、业务,缺一层都会出事

输入校验很多人只做最外面一层,结果就是“格式对了但业务错了”。我习惯把它拆成三层,每层的职责分清。

层级检查内容举例出错后果
语法层类型、长度、字符集、格式手机号必须 11 位数字脏数据入库、解析异常
语义层值是否在合理范围、是否互斥结束时间必须晚于开始时间业务逻辑错乱
业务层是否满足业务规则与权限约束该用户是否有这个商品的购买资格越权、资损

很多资损事故不是被攻击,而是业务层校验缺失。比如优惠券接口只校验了券存在、没过期,没校验“这个券是否属于当前用户”,批量遍历券号就能刷。这类问题扫描器扫不出来,只能靠人对着业务流程图一条条问“这一步如果用户换个 ID 会怎样”。

统一入口是我强烈建议的一件事。所有请求参数在进入业务逻辑之前,先经过一层校验器(很多框架里叫 DTO 或 Schema 校验),校验失败直接返回,不进业务代码。这样做的好处是校验逻辑集中、可审计,新人加接口时也不太容易漏。

3. 浏览器端的信任错位:XSS 与 CSRF

3.1 XSS 的本质:数据被当成了代码

XSS 一句话概括:本该作为文本展示的内容,被浏览器当成了可执行的代码。三种类型我在笔记里是这么区分的——存储型是脏数据进了数据库,谁看谁中招;反射型是脏数据在 URL 里,诱导点击才触发;DOM 型是前端 JS 自己把不可信数据塞进了innerHTML之类的危险 API,服务端根本不知情。

第三种最容易被忽略,因为后端日志里干干净净。我在排查一个前端渲染问题时遇到过:从接口拿到的标题经过一层模板渲染后插进页面,接口返回的是 JSON 字符串,看着无害,但前端用了直接插入 HTML 的方法,标签就被解析了。所以查 XSS 不能只查后端,得把前端的渲染路径也走一遍。

危害不只是弹个框。真正要命的是 XSS 运行在受害者的浏览器上下文里,能读到同域的 Cookie、能发起带凭证的请求、能改页面内容诱导操作。配合 CSRF 或者会话劫持,能直接接管账号。所以对 XSS 的态度我从来不宽容,哪怕只是一个“内部后台”,因为后台账号权限更高。

3.2 输出编码按上下文来,CSP 做兜底

防御的第一原则是输出编码,而不是输入过滤。原因很实在:同一条数据会在不同地方展示——HTML 正文、HTML 属性、JS 变量、CSS、URL 参数,每种上下文的转义规则都不一样。在输入时统一过滤,要么过度转义把正常内容搞坏,要么漏掉某种上下文。

输出位置编码方式常见错误
HTML 文本HTML 实体编码只转义尖括号,漏掉引号
HTML 属性属性值加引号 + 实体编码属性值不加引号
JS 变量JSON 序列化后嵌入直接拼字符串
URL 参数URL 编码手动拼接查询串
富文本白名单标签过滤用正则做黑名单过滤

富文本是唯一需要“过滤”而不是“编码”的场景,因为业务上确实要保留部分标签。这里的正确做法是白名单:允许哪些标签、哪些属性、哪些链接协议,一条条列出来,用成熟的库做,绝对不要自己写正则黑名单。黑名单永远会漏,比如<scr<script>ipt>这种嵌套写法,简单的一轮替换就会被绕过。

第二道防线是内容安全策略,也就是 CSP。它的作用是告诉浏览器“这个页面只允许加载哪些来源的脚本”。就算编码漏了,CSP 也能拦住大部分执行。

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-每次请求生成的随机值'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

落地上有个渐进策略:先上Content-Security-Policy-Report-Only,只上报不拦截,跑一两周看报告里有哪些内联脚本和第三方资源,逐个改造完再切成强制模式。直接上强制模式大概率会把页面搞挂,尤其是用了大量内联事件的存量项目。

3.3 CSRF:为什么 SameSite 和 Token 要一起用

CSRF 的成因是浏览器的“自动带凭证”机制:你访问了 A 站,又打开了恶意页面,恶意页面向 A 站发请求,浏览器会老老实实把 A 站的 Cookie 带上,服务端看起来就是一个合法请求。

防御的核心是证明这个请求是“我主动发的”,而不是“别人替我发的”。Cookie 的SameSite属性是第一道闸,Lax表示跨站发起的 POST 请求不带 Cookie,这能挡住绝大多数场景。

// 会话 Cookie 的属性组合,每一项都有明确目的 res.cookie("sid", token, { httpOnly: true, // JS 读不到,降低 XSS 盗取会话的收益 secure: true, // 只在 HTTPS 下传输 sameSite: "lax", // 跨站请求默认不带,挡住大部分 CSRF path: "/", maxAge: 1000 * 60 * 60 * 2 });

但SameSite不能单独依赖,原因是它依赖浏览器版本,而且同站不同子域的情况它挡不住。所以关键操作还要加第二层——CSRF Token 或者自定义请求头校验。我一般这样定:普通查询用SameSite=Lax就够了;涉及资金、权限变更、数据删除的操作,强制校验 Token,并且 Token 绑定会话、一次性有效。转账、改密这类操作再加一层二次确认(短信或密码),三层的成本不高,但能挡住绝大多数自动化攻击。

3.4 前端框架的“自动转义”不是万能护身符

用现代框架的人容易产生一种错觉:框架会自动转义,所以不用担心 XSS。框架确实把大部分文本插值做了转义,但它同时提供了显式的“绕过转义”出口,比如某些框架里的dangerouslySetInnerHTML、v-html、[innerHTML]。这些 API 是中性的,问题在于很多人用它渲染后端返回的“富文本”,而后端的富文本没做白名单过滤。

我的规矩是:代码里出现这类 API,必须能说清楚三件事——数据来源是谁、是否经过白名单过滤、过滤库的版本是否最新。说不清楚的一律改成纯文本展示。这条规矩在代码评审里很好用,因为它逼着写代码的人回答具体问题,而不是说“应该没问题”。

4. 身份、会话与密码:最容易被做糙的部分

4.1 越权:功能权限和数据权限必须分开查

越权分两类。垂直越权是低权限用户调用了高权限接口,比如普通用户调到了管理员才有的删除接口;水平越权是同级别用户互相访问对方的数据,比如把订单号从自己的改成别人的就能看别人的订单。

垂直越权的防御相对简单:接口上声明所需权限,网关或中间件统一拦截。难的是水平越权,因为它跟业务数据强耦合,框架帮不上忙。

我的做法是把“数据归属校验”变成一条硬性规则:凡是按 ID 查询、修改、删除的接口,必须先按“当前用户 + 资源 ID”联合查询,查不到就返回不存在。注意是“查不到”,而不是“无权限”,这样连资源是否存在都不泄露。

# 反面写法:先查再说,中间有一段时间和判断逻辑的漏洞面 order = db.get_order(order_id) if order.user_id != current_user.id: return {"code": 403} # 推荐写法:把归属条件放进查询本身 order = db.query_one( "SELECT * FROM orders WHERE id = %s AND user_id = %s", (order_id, current_user.id) ) if not order: return {"code": 404}

两种写法看着差别不大,但第二种把权限条件变成了数据查询的一部分,避免了“先取出来再判断”的过程中被遗漏分支。团队里我一般会在代码规范里明确写这条,评审时只看有没有把user_id带进查询条件里。

还有一类容易漏的场景是批量接口。单个接口做了校验,但批量的ids数组只校验了第一个或者干脆没校验,攻击者塞一百个 ID 进去就能捞到别人的数据。批量接口的正确做法是查出来之后比对数量,数量对不上就整批拒绝。

4.2 会话管理:细节决定账号守不守得住

会话管理的细节特别多,我把常查的几条列成了清单:

  • 登录成功后重新生成会话 ID,防止会话固定。旧 ID 必须失效。
  • 会话有绝对过期和空闲过期两个维度,只设一个都不够。
  • 敏感操作前重新验证身份,不依赖长期会话。
  • 退出登录时服务端销毁会话,而不是只清浏览器 Cookie。
  • 会话存储集中化,多实例部署时不能用内存会话,否则用户会被随机踢下线,运维为了省事可能就把超时设得极长。

这里有个我踩过的坑:早期项目里退出登录只做了前端清 Cookie,服务端会话还在有效期内。如果会话 ID 之前被泄露(比如日志里打印过),退出之后依然能用。后来改成服务端主动作废,并且在数据库里记录会话状态,才算干净。

注意事项:调试日志里千万不要打印完整 Cookie、Authorization 头、请求体中的密码字段。我见过日志平台里能直接搜到用户明文密码的,这属于自己给自己制造事故。

4.3 密码存储:慢哈希 + 参数要算不要抄

密码存储只有一个正确方向:不可逆的慢哈希 + 每条记录独立随机盐。快哈希(MD5、SHA 系列)绝对不行,因为现代硬件每秒能算几十亿次,加盐只能防彩虹表,防不住逐个爆破。

算法选择上,我倾向 bcrypt 或 Argon2id。bcrypt 兼容性好、库成熟;Argon2id 抗 GPU 能力更强,但要注意库的维护状态。具体参数不要照抄网上的数字,要自己测。

import bcrypt, time password = b"user-provided-password" for rounds in (10, 11, 12, 13): t0 = time.time() bcrypt.hashpw(password, bcrypt.gensalt(rounds=rounds)) print(rounds, round((time.time() - t0) * 1000), "ms")

跑一遍就能看到成本翻倍的规律:rounds 每加 1,耗时翻一倍。目标是单次验证控制在200~400 毫秒之间——对正常用户几乎无感,对暴力破解来说成本被放大了几个数量级。如果服务器配置低,取到 150 毫秒也能接受,但不能低于 100 毫秒。这个数字要在生产同规格的机器上测,开发机上测出来的结果没有参考价值。

算法是否抗 GPU参数建议备注
MD5 / SHA-256否不建议使用速度太快,只能做校验和
bcrypt一般cost 10~13有 72 字节长度限制,注意截断
Argon2id强按内存与时间双参数调需要确认依赖库维护活跃

另外两个细节:一是密码长度上限要设,比如 128 字符,防止超长输入把哈希计算拖成拒绝服务;二是不要自作聪明地做“密码强度正则”,那玩意儿经常把带特殊字符的强密码判成弱密码,用长度下限加常见弱口令黑名单更实际。

5. 文件上传、SSRF 与反序列化:高危功能点怎么收口

5.1 文件上传要过四道闸

文件上传是少数几个“做错了直接能拿到服务器权限”的功能,我一般要求过四道闸。

第一道,类型校验看内容不看后缀。后缀可以随便改,要读文件头判断真实类型,同时用白名单而不是黑名单。第二道,存储位置与执行权限分离。上传目录不能有脚本执行权限,最好放到独立的存储服务或静态资源域名下。第三道,文件名服务端重新生成,绝对不能用用户传的文件名,否则路径穿越和覆盖攻击都来了。第四道,大小与数量限制,防止存储被打满。

还有两个细节容易被忽略:图片要重新编码(顺便可以剥掉藏在元数据里的内容),压缩包要防解压炸弹(限制解压后的总大小和文件数量)。这两条在做过一次事故复盘之后,我加进了必查清单。

5.2 SSRF:出网这件事必须有白名单

SSRF 的成因是服务端替用户去访问 URL,比如图片抓取、Webhook 回调、在线预览。如果没做限制,用户就能让服务器去访问内网地址,把内网的服务探一遍。

防御的核心是出网白名单:只允许访问明确列出的域名或 IP 段,解析后的 IP 也要校验。这里有个经典绕过手法是 DNS 重绑定——第一次解析到合法 IP,校验通过后再解析到内网 IP。所以校验必须在“真正发起连接时”做,而不是在“参数校验时”做,最好在底层网络出口统一限制。

另一条是禁止跟随重定向,或者对每一跳都重新校验。曾经有个案例是首次请求地址合法,跳转后到了内网地址,校验只做了第一跳,直接绕过去了。

5.3 XXE 与反序列化:能不用就不用

这两类问题的共同点是“默认配置就是危险的”。XML 解析器默认会处理外部实体,反序列化默认会按数据里的类型信息去实例化对象。防御思路也很一致:关掉不需要的能力。

XML 解析时显式禁用外部实体和 DTD;反序列化尽量不用原生格式,改用 JSON 这类纯数据格式;如果必须用,就加类型白名单,只允许反序列化预先注册的类。这类配置我一般直接写进项目的基础模板里,因为靠人记是靠不住的,模板里默认关掉,要用的人得主动打开并说明理由。

6. 上线前怎么查:工具链与自检顺序

6.1 自动化能覆盖什么,覆盖不了什么

工具能查出来的:已知依赖漏洞、明显的配置问题、常见模式的注入点、响应头缺失。工具查不出来的:业务逻辑越权、权限设计缺陷、竞态条件、复杂的组合利用。我见过团队把扫描器报告清零就认为安全达标,结果上线三天爆出一个水平越权,非常典型。

所以我的做法是两条腿走路:自动化做“地毯式排查”,保证不遗漏低级问题;人工做“定向验证”,重点打业务逻辑。人力有限时,人工优先看三类接口——涉及金额的、涉及权限变更的、涉及数据导出的。这三类出问题损失最大。

6.2 依赖与配置扫描,直接进流水线

依赖漏洞是最容易自动化的一块,也是最容易拖延的一块。我的做法是把它做成流水线的硬性卡点,高危依赖不修就不让合并。

# 前端依赖审计 npm audit --production # Python 依赖审计 pip-audit -r requirements.txt # 静态规则扫描,只跑高置信度规则,避免报告淹没 semgrep --config=p/security-audit --severity=ERROR

这里有个经验:只跑高置信度规则。如果一上来把所有规则全开,报告里几百条误报,团队三天就没人看了。我的做法是先只开ERROR级别,把存量清干净,再逐级放开,每次放开都要能说清楚“这条规则在我们项目里对应什么风险”。

安全响应头这种配置项,直接在网关层统一加,不要指望每个应用自己配:

add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

注意always参数,不加的话错误响应(比如 4xx、5xx)不会带这些头,而错误页恰恰是最容易出问题的地方。

6.3 手工验证的顺序:先权限,后注入

手工验证我有一套固定顺序,因为它能保证在有限时间里覆盖风险最高的部分。

第一步,梳理角色与资源。列出系统里所有角色,以及每类资源归属谁。这一步花的时间最长,但收益最大,越权问题基本在这一步暴露。

第二步,用低权限账号走一遍高权限流程。抓包看接口层面的校验,而不是只看页面按钮有没有隐藏。按钮隐藏只是体验,接口校验才是安全。

第三步,看注入面。把所有接收参数的地方过一遍,重点看排序字段、分页参数、导出功能的筛选条件——这些地方最容易被拼接。

第四步,看上传与文件读取。上传测类型绕过和路径穿越,读取测能不能读到预期外的文件。

第五步,看错误信息。故意传错参数,看返回里有没有堆栈、SQL 片段、内网地址、组件版本号。生产环境应该只返回一句通用提示加一个追踪 ID,详细信息写日志里。

实操心得:手工验证时一定要用测试环境 + 自己造的测试数据,别拿生产数据练手。我见过有人在生产上做越权验证,把别人的订单状态改了,最后只能挨个道歉恢复。

7. 常见问题速查与几条踩坑心得

7.1 一张速查表,出问题先照着对

现象可能原因排查方向
页面出现来历不明的弹窗或跳转存储型 XSS查最近入库的富文本是否做了白名单过滤
用户反馈被莫名踢下线会话存储不共享或过期策略冲突查多实例会话存储、Cookie 域与路径设置
接口返回数据里出现别人的记录水平越权查查询条件是否带上了当前用户 ID
上传成功但文件访问 404存储目录与访问域名不一致查静态资源映射与执行权限配置
登录慢,CPU 飙高哈希成本设太高或未做限流测单次哈希耗时,加登录失败限流
第三方回调验签总失败签名串拼接顺序或编码不一致统一用原始请求体算签名,注意 URL 编码

7.2 几个我实实在在踩过的坑

第一个坑:以为参数化查询就万事大吉。有个报表接口,SQL 主体用了参数化,但排序字段是拼接的,而且还做了“智能容错”——传错的字段名会回退到默认值。这个容错逻辑让我一度以为没问题,直到把排序参数改成一段构造内容才发现拼接点就在那。教训是:注入排查要按“进入 SQL 的每一个变量”逐个过,而不是看这个函数用没用参数化。

第二个坑:前端校验当成了安全校验。一个导入功能,前端限制了文件类型和大小,后端直接读流处理。有人绕过前端传了个超大文件,直接把内存打满。从那以后我们定了一条规矩:所有前端校验必须在后端重做一遍,前端那份只是为了提升体验,日志里也不当作依据。

第三个坑:日志把敏感信息全打印了。排查线上问题时为了方便,把整个请求体打进了日志,里面包含手机号、身份证、密码。日志平台是按关键词检索的,等于把敏感数据做成了全文索引。后来改成统一脱敏中间件,只打印字段名和长度,出问题再用追踪 ID 去查。

第四个坑:过度信任内部服务。内部接口没做鉴权,理由是“只有内网能访问”。后来做安全评审时发现,只要有一个对外服务存在 SSRF,内网就变成了公共区域。现在的做法是内部接口也带服务身份凭证,网络隔离和身份校验两条腿走。

第五个坑:修复只修了报告里那一条。扫描器报了一个 XSS,开发改完那个页面就完事了,结果同一套模板在另外五个页面都在用。修漏洞要修“模式”,不是修“点位”。我们现在的要求是:同类问题必须全量搜索、全量修复,并在规范里补一条,防止新人再写出来。

最后再分享一个我在整理这份笔记时形成的习惯:每修完一个安全问题,不要只记“改了什么”,要记“为什么当时没发现”。前者是操作记录,后者才是能力沉淀。我这份笔记里最有价值的部分,不是那些漏洞原理,而是每一条后面的“当时是怎么漏掉的”——因为漏掉的原因往往高度重复,把这几个原因堵死,后面同类问题基本就不会再出现了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询