做后端开发这么多年,我一直觉得自己写代码挺靠谱的。直到某一天,服务器告警狂响,日志里出现了一堆奇奇怪怪的请求——有人在试图读取我的/etc/passwd,有人往我数据库里塞了一堆赌博网站的链接,还有人用我的服务器当跳板去挖矿。
那一刻我意识到:我写的接口,在黑客眼里跟敞开大门没什么区别。
这篇文章,是用真实教训换来的。后端常见安全漏洞,我挨个给你讲一遍,建议对照自查,有则改之无则加冕。
一、SQL 注入:你以为的「安全查询」,其实是个后门
先从一个经典场景说起。用户在页面上输入一个用户名,你后台这样写:
username := r.FormValue("username")
query := "SELECT * FROM users WHERE name = '" + username + "'"
row := db.QueryRow(query)
看起来很正常对吧?但是如果用户输入的是:
'; DROP TABLE users; --
你的 SQL 就变成了:
SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
恭喜你,用户表没了。
很多人觉得「我的接口有登录态,不可能有人来手动构造参数」。兄弟,别太天真。现在自动化扫描工具一抓一大把,攻击者跑一趟也就几分钟的事,你不在代码里做防护,人家直接用工具扫一遍,你所有没参数化的查询都会被扒个精光。
正确做法:所有 SQL 查询全部参数化,一个都不要裸写。不管是 Go 的 db.QueryRow + ? 占位符,还是 ORM 的链式调用,老老实实用参数绑定。
二、SSRF:你的服务器被当成跳板去「内网一日游」
SSRF(Server-Side Request Forgery)是什么?就是你信任了用户传入的 URL,然后你的服务器代替用户去访问了这个 URL。
典型场景:用户提供一个图片 URL,你的服务去抓取然后展示。代码大概长这样:
func FetchImage(url string) ([]byte, error) {
resp, err := http.Get(url)
// ...
}
看起来人畜无害对吧?但是攻击者可以给你传:
http://169.254.169.254/latest/meta-data/ # AWS 元数据服务
http://localhost:6379/ # Redis
http://192.168.1.1/admin # 内网管理后台
你的服务器「替」攻击者访问了这些内部地址,然后攻击者通过你的响应就拿到了内网数据。如果你的 Redis 没设密码(很多人觉得「内网不需要密码」),攻击者直接就能执行 CONFIG SET dir /root/.ssh 写入 SSH 公钥,拿到服务器权限。
正确做法:禁止服务器访问内网地址;对用户传入的 URL 做严格校验;使用域名白名单而非 IP 白名单(因为内网可能有 DNS rebinding 攻击)。
三、敏感信息泄露:你在日志里写的每一句话,都可能成为攻击者的线索
这个问题特别容易被忽略。我见过太多代码是这么写的:
log.Printf("用户 %s 登录失败,密码是: %s", username, password)
log.Printf("JWT secret 是: %s", jwtSecret)
log.Printf("数据库连接: %s", dsn)
首先,生产环境的日志可能会被打印到标准输出,被容器日志收集工具收集,甚至有时候日志文件会被意外上传到 GitHub 公开仓库。
其次,就算日志只在服务器上,攻击者如果通过其他漏洞拿到了代码执行权限,第一件事就是翻你的日志文件,找敏感信息。
更骚的是,有些开发者在报错信息里直接暴露了业务逻辑:
if user.Password != hash(password) {
return nil, fmt.Errorf("密码错误,请检查后重试")
}
// 攻击者可以通过不同的错误信息判断「用户名不存在」vs「密码错误」
// 进而枚举出你的有效用户名列表
正确做法:日志里永远不要出现密码、密钥、Token;统一错误信息,登录失败统一返回「用户名或密码错误」,不要区分具体是哪个字段出了问题。
四、越权访问:你以为校验了用户,但没校验「哪个用户」
这个问题在业务逻辑复杂的系统里极其常见。典型场景:用户想查看自己的订单详情,接口是 GET /orders/12345,代码里写了:
order := getOrderByID(orderID)
if order.UserID != currentUser.ID {
return errors.New("无权访问")
}
return order
看起来没问题啊?但如果攻击者把 orderID 改成 12346,而这个订单恰好属于另一个用户,而你系统里恰好有个接口返回了当前用户的所有订单列表——攻击者就能枚举所有订单 ID,逐个试探,拿到别人的订单信息。
这就是「水平越权」。还有一种更严重的叫「垂直越权」:普通用户通过某种手段访问到了管理员接口。
正确做法:所有查询类接口,必须校验资源所属关系,不能只校验「用户登录了」;接口权限控制要落到每个具体接口上,而不是前端藏着不让显示就完事了。
五、文件上传漏洞:图片不一定是图片,可能是「你服务器的钥匙」
文件上传功能也是重灾区。很多系统允许用户上传头像、附件,代码可能这么写:
file, header, _ := r.FormFile("avatar")
filename := header.Filename
dst, _ := os.Create("/uploads/" + filename)
io.Copy(dst, file)
攻击者上传一个名为 ../shell.php 的文件,如果你的存储路径没做安全处理,文件可能就被写到了 /uploads/../shell.php,也就是网站根目录。然后攻击者访问 https://yoursite.com/uploads/shell.php,直接拿到了服务器权限。
就算文件名做了处理,攻击者还可能在文件内容里埋 webshell,用图片马的方式把 PHP 代码写入一个看似正常的 JPG 文件里。
正确做法:上传文件重命名,用随机字符串替代原始文件名;存储路径隔离,永远不要把上传目录放在 Web 根目录;对上传文件做内容类型检测,不只是检测扩展名;上传目录禁用 PHP/CGI 等脚本执行权限。
写在最后
写到这里,我回想了一下自己踩过的坑,大概每一条都是血泪教训。安全这事,永远是「不知道才会出问题,知道了一般就不会犯浑」。
很多人觉得「我的站点小,没人会来攻击我」。说实话,现在黑客都是用工具自动扫描的,不挑目标,你裸奔在公网上,扫到就是赚到。你的接口安全与否,不取决于你站多大,而取决于你防护有多差。
所以——去检查一下你的代码,上面这五条,有则改之,无则加冕。别等服务器被清了数据再来后悔。
祝你代码无漏洞,服务器永不被黑。