主题
常见安全漏洞:沿着输入找到危险操作
审查一个上传、搜索或回调接口时,先追踪三件事:哪些值来自不可信输入,这些值最终控制了什么操作,中间有没有真正限制这种控制。用户输入进入 SQL 语法、HTML、命令行、网络地址或文件路径时,风险不同,修复方法也不同。
本文按这条路径讲 SQL 注入、XSS、CSRF、SSRF、路径穿越、命令注入、反序列化与开放重定向。所有运行实验都只接触本地临时文件、内存或 loopback 测试服务器,没有外部扫描,也没有真实受害目标。
OWASP Top 10 要带版本看
截至本文查证日 2026-10-13,本文采用 OWASP Top 10:2025 的正式列表。它是帮助开发者建立风险意识的分类,不是列完十项就能证明系统安全的验收标准。
| 2025 编号 | 官方类别 | 审查时先问的问题 |
|---|---|---|
| A01 | Broken Access Control,访问控制失效 | 主体是否有权操作这份对象? |
| A02 | Security Misconfiguration,安全配置错误 | 默认配置、暴露端口和错误页面是否扩大边界? |
| A03 | Software Supply Chain Failures,软件供应链失效 | 依赖、构建和制品来自哪里? |
| A04 | Cryptographic Failures,密码学失效 | 哪些数据需要保密和完整性保护? |
| A05 | Injection,注入 | 数据是否被解释成了指令或语法? |
| A06 | Insecure Design,不安全设计 | 业务流程本身是否允许滥用? |
| A07 | Authentication Failures,认证失效 | 系统是否认错人或保留了失效会话? |
| A08 | Software or Data Integrity Failures,软件或数据完整性失效 | 更新、消息、反序列化对象能否被替换? |
| A09 | Security Logging and Alerting Failures,安全日志和告警失效 | 发生违规后能否发现并追溯? |
| A10 | Mishandling of Exceptional Conditions,异常条件处理不当 | 超时、错误和资源耗尽时是否意外放行? |
这不是 2021 版的原样重排。例如 SSRF 在 2021 版是单列的 A10,2025 版的 A10 已变成异常条件处理。不要拿旧编号解释新报告;具体漏洞也可能涉及多个类别。下面按代码边界组织,而不是强行把每个漏洞塞进唯一格子。
先画输入到危险操作的路线
正在绘制图示…
查看图源代码
flowchart LR
I["不可信输入<br/>HTTP、文件、队列、第三方响应"] --> P["解析与结构校验"]
P --> B["业务校验与授权"]
B --> S{"最终操作是什么?"}
S --> Q["数据库:参数与SQL结构分离"]
S --> H["HTML:按输出上下文编码"]
S --> N["网络:限制目标与实际连接"]
S --> F["文件:受限目录中安全打开"]
S --> C["进程:固定程序与参数边界"]
这里常把危险操作叫 sink,把输入传播叫污点传播。记住这个词不是目的;要能指出“这个请求字段最后改变了哪一个操作”。校验姓名长度不能修复 SQL 拼接,HTML 转义也不能让文件路径安全。
| 漏洞 | 输入控制了什么 | 危险落点 | 主要防线 | 防线仍需配合什么 |
|---|---|---|---|---|
| SQL 注入 | SQL 语法的一部分 | 查询或写入执行 | 参数化值,结构部分允许列表 | 对象授权、数据库最小权限 |
| XSS | 页面中的可执行上下文 | HTML/DOM/脚本解释 | 上下文编码、安全 DOM API | CSP、可信模板、依赖治理 |
| CSRF | 借用户浏览器发起操作 | 自动附带凭据的状态变更接口 | 会话绑定 token、来源验证 | SameSite、无副作用 GET、XSS 防护 |
| SSRF | 服务器请求的目的地 | 出站网络连接 | 目标允许列表、解析后连接约束 | 出站隔离、重定向和代理治理 |
| 路径穿越 | 文件解析后的真实位置 | 打开、覆盖、解压文件 | 受限根目录 API、授权 | 符号链接、挂载与设备边界 |
| 命令注入 | 命令或选项 | shell / 子进程 | 不经 shell,固定程序和参数 | 参数语义、权限、资源限额 |
| 反序列化风险 | 还原对象和资源使用 | 解码器、类型回调、业务字段赋值 | 明确 schema、限制资源和可写字段 | 完整性验证、更新解码库 |
| 开放重定向 | 浏览器下一跳 | Location / 前端跳转 | 服务器定义的目标映射 | 授权回调精确匹配 |
SQL 注入:把数据交给参数,不让它变成语法
假设搜索框里的姓名被拼进 WHERE name = '输入'。数据库接到的是一整条 SQL,无法知道哪一部分原本被程序员当作“姓名”。一旦输入改变了引号或表达式结构,实际查询就偏离了原意。
Go 的做法是把 SQL 文本和参数值分别传给 database/sql,例如 db.QueryContext(ctx, "SELECT id FROM users WHERE name = ?", name)。PostgreSQL 驱动常用 $1,占位符要按驱动选择。不要用 fmt.Sprintf 或字符串拼接把值嵌进去,再期待驱动识别它原本是参数。Go 官方 SQL 注入指南
参数只能代替值,不能普遍代替表名、列名、排序方向。用户想选择排序字段时,应把 name、created_at 这样的外部选择映射到服务端写死的 SQL 片段,拒绝未知选项。ORM 的原始 SQL 接口、拼接排序语句和动态查询也要检查,不能以“用了 ORM”结束审查。OWASP SQL Injection Prevention
本文没有连接数据库,因此不展示伪造的“注入成功/修复成功”数据库输出。参数化调用说明依据官方 API;目标数据库、驱动与查询的集成验证仍需单独完成。
XSS:同一个字符串,落在不同位置就有不同含义
XSS 是不可信数据最终变成浏览器可执行内容。反射型通常来自当前请求,存储型先写进数据库再出现在其他页面,DOM 型则由前端代码把数据送进危险 DOM 操作。来源不同,关键仍是最后的解释上下文。
下面只用无害的 <b> 标签,比较同一模板交给 text/template 和 html/template。没有运行脚本,也没有打开浏览器。
go
const source = `<p>{{.}}</p>`
input := "<b>guest</b>"完整程序分别执行两种模板,本机实际输出:
text
text: <p><b>guest</b></p>
html: <p><b>guest</b></p>第一行保留了 HTML 结构;第二行把输入保留为文本。html/template 会根据模板上下文进行转义,前提是模板本身可信。把输入强转成 template.HTML、template.JS 或 template.URL,是在告诉库“这段已经可信”,不能把它当作消除警告的方法。Go 1.27.2 html/template 安全模型
HTML 文本、属性、URL、JavaScript、CSS 的规则不一样。一份“通用 escape”不能到处用。前端显示普通文本时优先使用 textContent;富文本必须按允许的标签、属性和 URL 协议做净化。CSP 可以减少部分执行机会,但不能替代正确编码;它的 nonce 需要按响应生成,不是写在代码里永远不变。OWASP XSS 防护、CSP Level 3
CSRF:凭据是浏览器自动带上的,操作却不是用户想发的
浏览器可能根据 Cookie 规则自动给目标站点附带凭据。攻击站点不一定需要读取响应,只要能诱使浏览器发出会改变状态的请求,就可能造成影响。因此“不允许跨域读响应”与“阻止跨站发起有凭据的操作”是两回事。OWASP CSRF 防护
常见设计是在服务端会话里保存不可预测的 CSRF token,由页面通过表单或自定义请求头回传,并检查请求的 Origin;缺少 Origin 时如何处理,要按客户端类型与 Referer/Fetch Metadata 等策略明确决定。GET 不应承担状态变更,SameSite Cookie 提供额外保护。无状态双提交 Cookie 方案也需要正确绑定会话并防止攻击者注入可伪造 Cookie,不能只比较两个同样由客户端控制的字段。
实验函数选择严格策略:Origin 必须等于 https://app.example,token 必须匹配受信会话中的值。测试中重复的 t 是公开占位数据,不是生产随机 token。
go
csrfAllowed("https://app.example", token, token)
csrfAllowed("https://other.example", token, token)text
csrf-good=true csrf-foreign=false它只是验证决策函数,未测试浏览器 Cookie 的 SameSite 行为。真正接口还要限制方法、绑定用户会话、限制请求体并处理缺失/过期 token。XSS 往往能在同源上下文发请求,不能指望 CSRF token 抵消 XSS。纯粹由客户端显式添加 Bearer 头且不自动携带的凭据,一般不具有相同的 Cookie CSRF 条件,但登录回调、Cookie 刷新和混合认证路径仍需逐项检查。
SSRF:检查过 URL,不等于限制住了连接
服务端替用户下载头像或抓取链接时,输入控制的是服务器的网络能力。服务器可能能访问用户无法直接访问的内部服务;SSRF 的危险来自这份权限被借用。只过滤 URL 中的 localhost 或几个字符串,无法覆盖地址表示、IPv6、DNS 变化、代理和重定向。OWASP SSRF 防护
对于只访问几个固定服务的业务,优先让用户提交资源 ID,服务端选择已配置地址。确实要接收 URL 时,应解析后检查 scheme、host、port,拒绝不需要的 userinfo 和其他格式;精确允许列表比域名后缀拼接更容易检查。
DNS 和重定向是两次容易漏掉的“重新选目标”
正在绘制图示…
查看图源代码
flowchart TD
U["输入 URL"] --> V["检查协议、主机、端口"]
V --> R["解析 A/AAAA,检查全部候选地址"]
R --> P["选定已通过检查的地址"]
P --> D["连接该地址,保留原主机的 TLS 身份验证"]
D --> H{"返回重定向?"}
H -- "默认拒绝" --> X["停止"]
H -- "业务必须允许" --> V
H -- "否" --> B["按时间和大小上限读取响应"]
如果校验阶段解析一次 DNS,连接时再按主机名解析一次,后一次答案可能不同。防护需要让实际拨号使用已验证的地址,或者通过具备相同约束的出站代理完成;TLS 仍要验证原服务主机名,不能为了连接固定 IP 而关闭证书验证。多地址结果要检查每个可能被拨号的地址。维护网络策略时还要处理回环、链路本地、私有地址、IPv4-mapped IPv6、云元数据及部署中的其他禁入网段;“不是 RFC1918 私网”不是“可以安全访问”的充分条件。Go Transport 拨号入口、netip 地址判断
实验只实现“固定主机 + 固定地址集合”的策略函数。192.0.2.10 来自文档专用 TEST-NET-1,只当公开测试值,程序绝不会连接它。相同主机的解析结果改为 loopback 时,实际输出为:
text
approved-address: true
changed-dns: false这证明函数检查了传入的解析结果,不证明真实 DNS rebinding 已被端到端防住。完整拨号固定、TLS、连接池、代理、DNS TTL 与目标轮换没有在这个函数中实现。若共享连接池,策略改变后旧连接仍可能复用,也要一并处理。
Go http.Client 默认会跟随部分重定向。实验用两个本地测试服务器,第一台只返回 302 指向第二台;客户端配置:
go
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},text
redirect-stopped=true target-hits=0结果说明第二个本地服务没被访问。生产下载器若允许重定向,每一跳都必须重新执行同样策略并限制跳数;不能只验证第一跳。还要约束总超时、响应头/体大小、解压后大小、并发量,避免“安全目标”返回无限内容造成资源耗尽。出站网络隔离是另一道防线,不应仅依赖 URL 校验。
路径穿越:清理字符串与限制文件能力是两件事
filepath.Clean 只整理路径的词法形式,不检查文件系统真实解析结果。先检查字符串前缀,再用 os.Open 打开,还可能被符号链接和检查后替换路径的竞态影响。压缩包解压同样要检查每个条目的路径与链接,不能因为压缩包文件本身在上传目录里就信任内部名称。
Go 1.24 引入的 os.Root 提供在指定目录树下操作文件的 API。本例在临时目录中创建一个受限根目录,根目录外放一个无害文本,再在根目录内创建指向该文本的相对符号链接。
go
r, err := os.OpenRoot(root)
// 省略的错误处理在完整代码中。
r.Open("../outside.txt")
r.Open("escape")本次 Linux/amd64 实测:
text
dotdot-rejected=true symlink-rejected=trueRoot 可以跟随仍在根内的相对符号链接,但拒绝逃出根。它不是操作系统沙箱:文档明确说它不禁止跨文件系统、Linux bind mount、/proc 特殊文件和设备文件。平台也有差异,例如 GOOS=js 的符号链接验证不能提供同等 TOCTOU 保证。应选受控上传目录,限制目录里能出现什么,并继续做对象授权。Go os.Root 文档、官方设计说明
命令注入:不经过 shell 只是第一道边界
Go exec.Command 默认不经过 shell,也不会自动解释分号、管道或通配符。若把输入拼进 sh -c 的命令文本,就重新引入 shell 语法解释。能用 Go 库完成文件、压缩或文本处理时,优先直接调用库,减少一层解释器。os/exec 包说明
本地实验调用固定的 /usr/bin/printf,格式串也是固定的 %s,输入作为独立参数:
go
exec.Command("/usr/bin/printf", "%s", "hello; printf INJECTED").Output()text
literal-argument: hello; printf INJECTED分号被原样打印,没有第二条命令被执行。即便不经过 shell,参数仍可能被目标程序解释成危险选项,所以要固定可执行文件、限制参数语义,并在目标程序支持时使用 -- 结束选项。不要允许输入指定程序路径;对确实需要的子进程设置时间、权限、工作目录、环境和输出上限。本例只验证参数边界,没有执行任何恶意命令。
反序列化:类型安全不等于输入安全
某些平台的反序列化可以触发类型构造、魔术方法或回调,形成危险执行链。Go 的普通 JSON 解码不会凭空获得其他语言的全部对象执行机制,但仍存在资源消耗、类型自定义解码逻辑和业务字段覆盖的问题。把请求直接解码到含 Role、TenantID 的数据库对象,再整体保存,就是常见的越权字段赋值。
应定义只包含客户端可写字段的输入结构,在读取前限制体积,解码后检查数值、长度、枚举和业务条件。下面的实验只接受一个 JSON 值,拒绝未知字段与尾随第二份 JSON:
go
d := json.NewDecoder(strings.NewReader(raw))
d.DisallowUnknownFields()
// Decode 输入结构后,再 Decode 一次,必须得到 io.EOF。text
unknown-field-rejected=true trailing-json-rejected=true这不是“严格 JSON 的全部要求”:传统 encoding/json 仍有大小写不敏感匹配、重复键处理、非法 UTF-8 替换等兼容行为。如果签名、网关和后端用了不同解析器,必须统一语义或拒绝歧义,不能假定 DisallowUnknownFields 也拒绝重复键。HTTP 接口应先用 http.MaxBytesReader 等限制读取,而不是先读进内存再检查长度;本例输入已在内存中,只验证解码逻辑。Go JSON 安全注意事项
encoding/gob 的官方文档明确指出它不是为对抗恶意输入加固的解码器,资源上限也不可配置,因此不应把它随手暴露给不可信网络输入。gob Security 对其他序列化格式,要确认 schema、允许类型、深度/体积限制和库的安全维护状态;给不可信数据签名也不能挽救已泄漏的签名密钥。OWASP 反序列化指南
开放重定向:让输入选目标 ID,比让它提供任意 URL 简单
登录后的 next 参数、邮件中的跳转页和前端路由都可能接受一个目标地址。若服务端把任意输入写进 Location,攻击者可以借可信站点域名包装外部钓鱼跳转;在授权流程里,还可能影响授权码或令牌的去向。OWASP 重定向防护
本例只接受 account、home 这样的键,由服务器映射成 /account、/。用户输入外部 URL 时不生成目标:
text
external-redirect-rejected: true若业务确实要接收站内路径,必须按解析后的规则限制,防止把 //host 等形式误当普通相对路径;目标页本身也要做授权。OAuth 回调应与预注册地址按协议精确匹配,不能套用一个宽松的“同域名就行”判断。
修复后,测试应当证明哪一条边界
一个漏洞修复至少需要正常输入仍可用、危险输入被拒绝、替代入口也受同样规则约束。测试名称最好指向边界,例如“跨租户不可读”“重定向不访问第二跳”“符号链接不可逃出根”,比笼统的 TestSecurity 更容易在后续改动中保留意图。
本文的测试能复现模板输出、局部地址策略、停止本地重定向、根目录限制、字面参数传递、CSRF 决策、JSON 边界和跳转映射。它们没有代替浏览器、真实网络、数据库和部署测试。审查时继续沿输入追到真正执行点,才能知道修复覆盖了哪一段,而不是看到一个过滤函数就停止。
依据与验证边界
- 风险分类:OWASP Top 10:2025;各漏洞的官方 Cheat Sheet 已在对应章节就近链接。
- Go API:Go 1.27.2 的 html/template、net/http、net/netip、os.Root、os/exec、encoding/json、encoding/gob;路径设计另见 Go 官方 osroot 文章。
- 实验:2026-10-13,官方 Go 1.27.2、Linux/amd64;
go run .、go test -race -count=1 ./...、go vet ./...实际通过。命令行实验需要 Linux 的/usr/bin/printf;路径实验需要本地符号链接支持。 - 所有文件操作限于程序创建的临时目录,网络实验限于 httptest 创建的 loopback 服务。未进行外部连接、端口扫描、真实账号攻击或系统安全设置修改。
- 未测试真实数据库注入、浏览器 XSS/CSRF/CORS、真实 DNS 重绑定、完整安全下载器、压缩包资源炸弹和生产沙箱。不能从本例输出推导“系统不存在这些漏洞”。
完整复现代码
代码中的域名、地址、token 都是公开测试夹具。allowedTarget 只验证传入的地址列表,不执行 DNS 解析或拨号;不要把它单独当作 SSRF 完整防护。
go.mod
go.mod
module example.com/security03
go 1.27.2main.go
代码块 · 160 行 · 点击收起 / 展开
go
package main
import (
"bytes"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
texttemplate "text/template"
)
func render() (string, string, error) {
const source = `<p>{{.}}</p>`
input := "<b>guest</b>"
var raw, safe bytes.Buffer
if err := texttemplate.Must(texttemplate.New("raw").Parse(source)).Execute(&raw, input); err != nil {
return "", "", err
}
if err := template.Must(template.New("safe").Parse(source)).Execute(&safe, input); err != nil {
return "", "", err
}
return raw.String(), safe.String(), nil
}
// A deliberately narrow destination policy. 192.0.2.10 is a public TEST FIXTURE
// address from TEST-NET-1; nothing here dials it or resolves external DNS.
func allowedTarget(raw string, resolved []netip.Addr) bool {
u, err := url.Parse(raw)
if err != nil || u.Scheme != "https" || u.User != nil || u.Fragment != "" || u.Hostname() != "assets.example" || (u.Port() != "" && u.Port() != "443") {
return false
}
if len(resolved) == 0 {
return false
}
expected := netip.MustParseAddr("192.0.2.10")
for _, ip := range resolved {
if ip.Unmap() != expected {
return false
}
}
return true
}
func csrfAllowed(origin, got, sessionToken string) bool {
return origin == "https://app.example" && len(sessionToken) >= 32 && len(got) == len(sessionToken) && subtle.ConstantTimeCompare([]byte(got), []byte(sessionToken)) == 1
}
type Input struct {
Name string `json:"name"`
}
func decode(raw string) error {
if len(raw) > 128 {
return errors.New("body too large")
}
d := json.NewDecoder(strings.NewReader(raw))
d.DisallowUnknownFields()
var in Input
if err := d.Decode(&in); err != nil {
return err
}
var extra any
if err := d.Decode(&extra); err != io.EOF {
return errors.New("expected one JSON value")
}
if len(in.Name) == 0 || len(in.Name) > 20 {
return errors.New("invalid name")
}
return nil
}
func destination(key string) (string, bool) {
p, ok := map[string]string{"account": "/account", "home": "/"}[key]
return p, ok
}
func pathChecks() (bool, bool, error) {
dir, err := os.MkdirTemp("", "path-boundary-")
if err != nil {
return false, false, err
}
defer os.RemoveAll(dir)
root := filepath.Join(dir, "root")
if err = os.Mkdir(root, 0700); err != nil {
return false, false, err
}
if err = os.WriteFile(filepath.Join(dir, "outside.txt"), []byte("public fixture"), 0600); err != nil {
return false, false, err
}
if err = os.Symlink("../outside.txt", filepath.Join(root, "escape")); err != nil {
return false, false, err
}
r, err := os.OpenRoot(root)
if err != nil {
return false, false, err
}
defer r.Close()
f, e1 := r.Open("../outside.txt")
if e1 == nil {
f.Close()
}
g, e2 := r.Open("escape")
if e2 == nil {
g.Close()
}
return e1 != nil, e2 != nil, nil
}
func redirectChecks() (bool, int32, error) {
var hits atomic.Int32
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1); w.WriteHeader(204) }))
defer target.Close()
source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, http.StatusFound) }))
defer source.Close()
c := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
resp, err := c.Get(source.URL)
if err != nil {
return false, hits.Load(), err
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusFound, hits.Load(), nil
}
func literalArg() (string, error) {
b, err := exec.Command("/usr/bin/printf", "%s", "hello; printf INJECTED").Output()
return string(b), err
}
func main() {
raw, safe, err := render()
must(err)
fmt.Println("text:", raw)
fmt.Println("html:", safe)
fmt.Println("approved-address:", allowedTarget("https://assets.example/a", []netip.Addr{netip.MustParseAddr("192.0.2.10")}))
fmt.Println("changed-dns:", allowedTarget("https://assets.example/a", []netip.Addr{netip.MustParseAddr("127.0.0.1")}))
blocked, hits, err := redirectChecks()
must(err)
fmt.Printf("redirect-stopped=%v target-hits=%d\n", blocked, hits)
dot, link, err := pathChecks()
must(err)
fmt.Printf("dotdot-rejected=%v symlink-rejected=%v\n", dot, link)
arg, err := literalArg()
must(err)
fmt.Println("literal-argument:", arg)
token := strings.Repeat("t", 32)
fmt.Printf("csrf-good=%v csrf-foreign=%v\n", csrfAllowed("https://app.example", token, token), csrfAllowed("https://other.example", token, token))
fmt.Printf("unknown-field-rejected=%v trailing-json-rejected=%v\n", decode(`{"name":"a","role":"admin"}`) != nil, decode(`{"name":"a"}{"name":"b"}`) != nil)
_, ok := destination("https://other.example")
fmt.Println("external-redirect-rejected:", !ok)
}
func must(err error) {
if err != nil {
panic(err)
}
}main_test.go
代码块 · 57 行 · 点击收起 / 展开
go
package main
import (
"net/netip"
"strings"
"testing"
)
func TestBoundaries(t *testing.T) {
raw, safe, err := render()
if err != nil || raw != "<p><b>guest</b></p>" || safe != "<p><b>guest</b></p>" {
t.Fatal(raw, safe, err)
}
dot, link, err := pathChecks()
if err != nil || !dot || !link {
t.Fatal(dot, link, err)
}
blocked, hits, err := redirectChecks()
if err != nil || !blocked || hits != 0 {
t.Fatal(blocked, hits, err)
}
arg, err := literalArg()
if err != nil || arg != "hello; printf INJECTED" {
t.Fatal(arg, err)
}
}
func TestTargetPolicy(t *testing.T) {
good := []netip.Addr{netip.MustParseAddr("192.0.2.10")}
for _, raw := range []string{"http://assets.example/a", "https://assets.example:444/a", "https://assets.example.other/a", "https://user@assets.example/a", "https://127.0.0.1/a", "https://assets.example/a#fragment"} {
if allowedTarget(raw, good) {
t.Fatal("accepted", raw)
}
}
if allowedTarget("https://assets.example", nil) {
t.Fatal("empty DNS")
}
if allowedTarget("https://assets.example", append(good, netip.MustParseAddr("::1"))) {
t.Fatal("mixed DNS")
}
}
func TestInputAndCSRF(t *testing.T) {
if decode(`{"name":"ok"}`) != nil {
t.Fatal("valid rejected")
}
for _, raw := range []string{`{"name":"a","role":"admin"}`, `{"name":"a"}{}`, `{"name":""}`, strings.Repeat("x", 129)} {
if decode(raw) == nil {
t.Fatal("accepted", raw)
}
}
token := strings.Repeat("t", 32)
if !csrfAllowed("https://app.example", token, token) || csrfAllowed("https://app.example", "", token) || csrfAllowed("", token, token) {
t.Fatal("csrf")
}
if p, ok := destination("account"); !ok || p != "/account" {
t.Fatal("redirect")
}
}运行命令
sh
go mod download
go run .
go test -race -count=1 ./...
go vet ./...