第一章:Go语言Web开发安全盲区大起底(2024最新CVE验证版)
Go凭借其简洁语法与高并发能力成为云原生Web服务首选,但开发者常误以为“标准库即安全”,忽视底层HTTP处理、内存模型及第三方依赖引入的真实风险。2024年已公开的17个高危CVE中,有9个直接关联Go生态典型误用模式——包括net/http头解析缺陷、encoding/json反序列化绕过、以及go-sql-driver/mysql连接池凭证泄露等。
HTTP头注入与响应拆分陷阱
Go的http.ResponseWriter.Header().Set()默认不校验换行符。攻击者构造恶意User-Agent: foo\r\nSet-Cookie: session=stolen可触发响应拆分(CRLF Injection)。修复方式必须显式过滤控制字符:
func sanitizeHeader(value string) string {
// 移除CR/LF/FF/TAB/VT等控制字符(RFC 7230)
return strings.Map(func(r rune) rune {
switch r {
case '\r', '\n', '\f', '\t', '\v':
return -1 // 删除
default:
return r
}
}, value)
}
// 使用示例:w.Header().Set("X-Trace", sanitizeHeader(r.Header.Get("X-Trace")))
JSON反序列化中的类型混淆漏洞
json.Unmarshal对interface{}字段默认启用宽松解码,当结构体含json.RawMessage或嵌套map[string]interface{}时,可能绕过类型校验触发远程代码执行(如CVE-2024-24786)。强制指定目标类型并禁用未知字段:
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // 拒绝未定义字段
var req struct {
ID int `json:"id"`
Data []byte `json:"data"` // 避免使用 json.RawMessage 或 interface{}
}
if err := decoder.Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
常见危险依赖风险速查表
| 包名 | 高风险版本 | 缓解措施 |
|---|---|---|
golang.org/x/text |
升级至v0.15.0+(修复Unicode规范化绕过) | |
github.com/gorilla/sessions |
启用Options{HttpOnly: true, Secure: true} |
|
github.com/spf13/cobra |
避免在PersistentPreRun中直接解析用户输入 |
所有修复均需配合静态扫描(gosec -exclude=G104,G107 ./...)与运行时监控(httptrace.ClientTrace记录重定向链)进行闭环验证。
第二章:HTTP层漏洞:从协议误解到真实攻击链
2.1 Content-Type与MIME类型混淆导致的XSS绕过(CVE-2023-46845复现实验)
当服务端仅校验 Content-Type: text/html 而忽略实际 MIME 解析逻辑时,攻击者可利用浏览器 MIME 嗅探行为触发 XSS。
复现关键请求
POST /upload HTTP/1.1
Content-Type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"/>
Content-Type: image/svg+xml声明为图像类型,但现代浏览器(Chrome/Firefox)在响应未设X-Content-Type-Options: nosniff时,会基于内容体嗅探为text/html并执行脚本;- 服务端若仅依赖 header 判断文件类型,将跳过 HTML 标签过滤逻辑。
混淆路径对比
| 检查维度 | 安全行为 | CVE-2023-46845缺陷 |
|---|---|---|
| Header检查 | Content-Type 匹配白名单 |
仅匹配 header,不解析 body |
| MIME实际解析 | 由浏览器动态 sniff | 服务端未模拟该行为 |
graph TD
A[客户端上传SVG] --> B{服务端检查Content-Type}
B -->|仅header匹配| C[存入静态资源目录]
C --> D[浏览器加载时嗅探为text/html]
D --> E[XSS执行]
2.2 HTTP/2快速重置攻击与Go net/http服务器连接状态竞争(CVE-2024-24789深度分析)
攻击原理简述
攻击者在单个 TCP 连接上并发发起大量 HTTP/2 请求,随即对其中部分流(stream)发送 RST_STREAM 帧,触发 Go net/http 服务端在流清理与连接读取间的状态竞态。
关键竞态点
serverConn的streamsmap 未加锁读写stream.reset()异步调用sc.closeStream(),而sc.readFrames()同时尝试分配新 stream
// src/net/http/h2_bundle.go:1523(简化)
func (sc *serverConn) closeStream(st *stream, err error) {
sc.serveG.check() // 非原子:检查 goroutine 身份但不保护 map
delete(sc.streams, st.id) // 竞态写入
st.body.Close() // 可能触发 readLoop 中的 body.Read()
}
该函数在无互斥保护下修改 sc.streams,而 readFrames 正在遍历同一 map 并调用 sc.newStream(),导致 panic 或 use-after-free。
修复策略对比
| 方案 | 锁粒度 | 性能影响 | 安全性 |
|---|---|---|---|
全局 sc.mu 保护 streams |
高 | 显著(串行化所有流操作) | ✅ |
| 细粒度 per-stream mutex | 中 | 较低 | ⚠️(仍存边界竞态) |
| RCU 风格引用计数 | 低 | 最小 | ✅(Go 1.22+ 采用) |
状态迁移流程
graph TD
A[收到 HEADERS] --> B[创建 stream 并插入 sc.streams]
B --> C{是否收到 RST_STREAM?}
C -->|是| D[异步 closeStream → delete sc.streams]
C -->|否| E[正常处理 DATA]
D --> F[readFrames 可能 panic: 'assignment to entry in nil map']
2.3 Host头注入与Go标准库ProxyHandler的反向代理信任链缺陷(含gin/echo框架实测PoC)
Go net/http/httputil.NewSingleHostReverseProxy 默认无条件信任客户端传入的 Host 头,导致攻击者可篡改 Host 值,劫持后端请求目标。
漏洞触发链
- 客户端发送
Host: attacker.com ProxyHandler将其透传至上游,覆盖req.URL.Host- 后端服务依据该
Host执行路由、日志、CORS 策略等敏感逻辑
Gin 实测 PoC 片段
r := gin.Default()
r.Any("/proxy", func(c *gin.Context) {
proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: "127.0.0.1:8081"})
// ❗未重写 Host 头,信任原始请求
proxy.ServeHTTP(c.Writer, c.Request)
})
此处
c.Request.Host直接被 ProxyHandler 用作上游目标域名,未做白名单校验或强制覆写。
修复建议(三选一)
- ✅ 强制覆写
req.Host = "trusted-backend" - ✅ 使用
Director函数清除/重设Host头 - ✅ 配合
Header.Set("X-Forwarded-Host", ...)留痕审计
| 方案 | 是否阻断注入 | 是否保留原始 Host 可见性 |
|---|---|---|
覆写 req.Host |
是 | 否 |
Director + 白名单校验 |
是 | 是(需额外记录) |
2.4 跨域配置失效:Go原生CORS中间件与第三方库的Origin校验逻辑漏洞(CVE-2024-30287复现与修复对比)
漏洞成因:Origin 头的空格绕过
Go 标准库 net/http 对 Origin 请求头未做规范化预处理,攻击者可构造 Origin: https://evil.com(末尾含空格)绕过白名单校验。
// 错误示例:直接字符串比较,忽略空白符
if r.Header.Get("Origin") == "https://trusted.com" {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
}
逻辑分析:
strings.EqualFold或strings.TrimSpace缺失,导致"https://trusted.com "≠"https://trusted.com";AllowOriginFunc若未显式裁剪,即触发 CVE-2024-30287。
修复方案对比
| 方案 | 是否修复空格绕过 | 是否支持通配符动态匹配 | 推荐度 |
|---|---|---|---|
github.com/rs/cors v1.8.1+ |
✅(自动 TrimSpace) |
✅ | ⭐⭐⭐⭐ |
原生 http.HandlerFunc 手写中间件 |
✅(需手动 TrimSpace) |
✅ | ⭐⭐⭐ |
gin-contrib/cors v1.5.0 |
❌(旧版未处理) | ✅ | ⚠️(需升级) |
修复代码(推荐)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := strings.TrimSpace(r.Header.Get("Origin"))
if origin == "" || !isTrustedOrigin(origin) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
next.ServeHTTP(w, r)
})
}
参数说明:
strings.TrimSpace消除首尾空白;isTrustedOrigin()应基于url.Parse校验 scheme/host,而非简单子串匹配。
2.5 请求体解析竞态:multipart/form-data解析中的临时文件残留与路径遍历组合利用(Go 1.22+ runtime行为验证)
竞态根源:MultipartReader 与 TempFile 生命周期错位
Go 1.22+ 中,multipart.Reader.NextPart() 在解析 filename 后立即创建临时文件(通过 os.CreateTemp),但若后续 part.Close() 未被显式调用(如 panic、超时或提前 return),该文件将滞留于系统临时目录。
残留文件 + 路径遍历 = RCE 风险链
攻击者构造恶意 filename="../../etc/passwd",配合并发上传触发竞态:
- 主 goroutine 在
ParseMultipartForm中写入临时文件; - 另一 goroutine 通过
/tmp/xxx路径直接读取未清理的残留文件。
// 示例:不安全的解析逻辑(Go 1.22+)
func handleUpload(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(32 << 20); err != nil {
http.Error(w, "parse err", http.StatusBadRequest)
return // ❌ 忘记清理,临时文件残留
}
// ... 处理逻辑(可能 panic 或提前返回)
}
逻辑分析:
ParseMultipartForm内部调用r.MultipartReader()→NextPart()→createTempFile(filename)。filename未经路径净化即参与os.CreateTemp,且 Go 运行时不再自动清理未关闭的*multipart.Part所关联的临时文件(1.22+ 明确移除隐式 cleanup)。
关键修复策略对比
| 方案 | 是否阻断竞态 | 是否防御路径遍历 | 说明 |
|---|---|---|---|
filepath.Clean(filename) |
❌ | ✅ | 仅净化路径,不解决文件残留 |
defer part.Close() |
✅ | ❌ | 解决竞态,但需手动确保每 part 关闭 |
r.FormValue("file") 替代 r.MultipartForm.File |
✅ | ✅ | 绕过临时文件机制,推荐 |
安全解析流程(mermaid)
graph TD
A[收到 multipart/form-data] --> B{解析 filename}
B --> C[filepath.Clean<br>并校验路径深度]
C --> D[调用 part.Open()]
D --> E[显式 defer part.Close()]
E --> F[流式处理至内存/安全位置]
第三章:内存与并发安全:Goroutine泄漏与非安全指针陷阱
3.1 context.Context取消传播失效引发的goroutine永久泄漏与DoS(结合pprof火焰图定位实战)
症状:火焰图中 http.HandlerFunc 下持续堆积 runtime.gopark
当 context.WithCancel 创建的子 context 未被正确传递至下游 goroutine,或 select 中遗漏 ctx.Done() 分支,取消信号无法穿透——导致 goroutine 永久阻塞在 chan recv 或 time.Sleep。
典型错误模式
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
go func() { // ❌ 未接收 ctx.Done()
time.Sleep(10 * time.Second) // 永不响应 cancel
log.Println("work done")
}()
}
逻辑分析:该 goroutine 完全脱离 context 生命周期管理;即使 HTTP 连接已断开(
r.Context()已 cancel),协程仍静默运行。参数time.Sleep无中断机制,ctx作用域仅限于 handler 栈帧,未逃逸至 goroutine 内部。
pprof 定位关键线索
| 指标 | 异常表现 |
|---|---|
goroutine profile |
数量随请求线性增长 |
top -cum |
runtime.gopark 占比 >95% |
web 图谱 |
http.HandlerFunc → time.Sleep 长链无 ctx.Done() 节点 |
修复方案(双保险)
- ✅ 显式监听
ctx.Done()并return - ✅ 使用
context.WithTimeout替代裸time.Sleep
go func(ctx context.Context) {
select {
case <-time.After(10 * time.Second):
log.Println("work done")
case <-ctx.Done(): // ✅ 取消传播生效
return
}
}(r.Context())
3.2 unsafe.Pointer与reflect包滥用导致的内存越界读写(CVE-2024-24791在序列化中间件中的触发路径)
核心漏洞成因
CVE-2024-24791 源于序列化中间件中对 unsafe.Pointer 的非受控类型转换,配合 reflect.Value.UnsafeAddr() 返回非法内存地址。
关键触发代码
func serialize(v interface{}) []byte {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
ptr := unsafe.Pointer(rv.UnsafeAddr()) // ⚠️ 对非导出字段/零值结构体调用未校验
return (*[8]byte)(ptr)[:rv.Size()] // 越界读取后续内存页
}
rv.UnsafeAddr() 在 rv 为零值、未寻址(CanAddr() == false)或指向栈上临时对象时返回无效指针;强制转换后切片越界访问,触发 UAF 或信息泄露。
影响链路
graph TD
A[用户传入空结构体] –> B[reflect.ValueOf → 零值Elem]
B –> C[UnsafeAddr返回栈顶伪地址]
C –> D[unsafe.Slice越界读8字节]
D –> E[泄露相邻goroutine栈数据]
| 场景 | CanAddr() | UnsafeAddr()有效? | 是否触发CVE |
|---|---|---|---|
| 导出字段结构体指针 | true | ✅ | 否 |
| 字面量struct{}{} | false | ❌(未定义行为) | 是 |
| reflect.Zero(Type) | false | ❌ | 是 |
3.3 sync.Pool误用与跨请求数据污染:Go 1.21+ Pool对象生命周期变更带来的安全风险
数据同步机制
Go 1.21 起,sync.Pool 的 Put 行为不再保证立即释放对象——它可能被延迟回收至下次 GC 周期,且同一 goroutine 多次 Get 可能复用旧对象而未清零。
典型误用示例
var bufPool = sync.Pool{
New: func() interface{} { return &bytes.Buffer{} },
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
buf := bufPool.Get().(*bytes.Buffer)
buf.WriteString("user:") // ✅ 首次写入
buf.WriteString(r.URL.Query().Get("id")) // ❌ 未 Reset,残留前次请求数据!
w.Write(buf.Bytes())
bufPool.Put(buf) // 污染已存入池的实例
}
逻辑分析:
buf未调用buf.Reset(),导致Put后该Buffer仍含上一请求的Bytes();下一次Get()可能直接返回该脏实例。参数r.URL.Query().Get("id")若为空,更易暴露前序残留内容(如"user:123admin")。
安全加固建议
- ✅ 每次
Get后强制Reset()或Truncate(0) - ✅ 避免在
sync.Pool中存储含内部状态的结构体(如*http.Request) - ✅ Go 1.21+ 应启用
-gcflags="-m"检查逃逸,确认 Pool 对象未意外逃逸至堆
| Go 版本 | Put 后对象可见性 | 是否需显式 Reset |
|---|---|---|
| ≤1.20 | 立即对当前 P 可见 | 推荐 |
| ≥1.21 | 延迟至 GC 周期 | 必须 |
第四章:依赖与生态风险:模块签名、供应链投毒与第三方库后门
4.1 Go Module Proxy缓存投毒与sum.golang.org签名绕过(CVE-2024-29155本地复现与防御策略)
复现关键路径
攻击者可篡改 GOPROXY 指向恶意代理,在模块下载时注入伪造 .info/.mod 文件,同时拦截对 sum.golang.org 的校验请求并返回预计算的合法哈希(实则对应恶意代码)。
核心PoC片段
# 启动可控代理(模拟投毒)
go env -w GOPROXY="http://localhost:8080"
go env -w GOSUMDB=off # 绕过sum.golang.org(非必需,但降低检测面)
此配置禁用校验数据库,使
go get完全信任代理返回的模块元数据与归档包,为哈希伪造创造前提。
防御矩阵
| 措施 | 作用域 | 强制性 |
|---|---|---|
启用 GOSUMDB=sum.golang.org |
全局校验 | ✅ 推荐 |
使用 GOPROXY=https://proxy.golang.org,direct |
回退至可信源 | ✅ |
go mod verify 定期扫描 |
项目级完整性验证 | ⚠️ 辅助 |
数据同步机制
graph TD
A[go get example.com/pkg] --> B{GOPROXY?}
B -->|是| C[Fetch .mod/.zip from proxy]
B -->|否| D[Direct fetch from VCS]
C --> E[Check sum.golang.org via GOSUMDB]
E -->|Fail| F[Abort]
E -->|OK| G[Cache & build]
4.2 第三方中间件高危CVE聚合:chi/v5路由参数解析漏洞(CVE-2023-48795)与fiber v2.49.0 Cookie加密密钥硬编码分析
漏洞成因对比
| 组件 | CVE编号 | 根本原因 | 触发条件 |
|---|---|---|---|
chi/v5 |
CVE-2023-48795 | 路由参数未校验嵌套路径分隔符 // |
r.Get("/user/{id}", handler) + 请求 /user//admin |
fiber |
—(未分配CVE,但属高危配置缺陷) | v2.49.0 中 config.EncryptionKey 默认值硬编码为 "secret" |
启用 CookieSecure 且未显式覆盖密钥 |
chi 路由解析异常示例
// chi/v5/router.go 片段(v5.1.3 修复前)
func (r *Mux) findRoute(method, path string) (*node, map[string]string) {
// ⚠️ path = "/user//admin" → tokens = ["user", "", "admin"]
tokens := strings.Split(path, "/") // 未过滤空字符串
// 后续匹配逻辑将 "" 误判为有效参数值,导致越界覆盖
}
该逻辑使 /{id} 捕获空字符串,破坏参数边界,可绕过身份校验或触发 panic。
fiber 密钥硬编码风险
// fiber/v2.49.0/config.go(问题版本)
var DefaultConfig = Config{
EncryptionKey: "secret", // ❌ 生产环境绝对禁止硬编码
}
攻击者获知密钥后可伪造任意签名 Cookie,实现会话劫持。需强制覆盖为 32 字节随机密钥。
4.3 go.sum完整性校验盲区:间接依赖替换攻击与go list -m -json输出欺骗检测
go.sum 仅记录直接依赖及其传递依赖的最终版本哈希,但不锁定中间模块路径或构建时实际解析的模块图。
间接依赖替换攻击原理
攻击者可发布恶意模块 github.com/evil/lib v1.2.0,其 go.mod 声明 require github.com/good/util v1.0.0;而真实 github.com/good/util v1.0.0 已被撤回,代理返回篡改版——该替换不改变 go.sum 中 github.com/good/util 的哈希值(因哈希仍对应原版声明),却绕过校验。
检测欺骗的关键信号
使用 go list -m -json all 可暴露不一致:
go list -m -json all | jq 'select(.Replace != null)'
✅ 输出非空表示存在
replace指令或 proxy 注入的重写路径;⚠️ 若.Version与.Dir中实际 commit 不匹配,则为典型欺骗。
| 字段 | 含义 | 攻击场景示例 |
|---|---|---|
Version |
go.mod 声明版本 |
v1.0.0(合法声明) |
Dir |
本地模块根路径 | /tmp/goproxy/evil-util@v1.0.0(恶意镜像) |
Replace |
是否被显式替换 | { "Old": { "Path": "github.com/good/util" } } |
graph TD
A[go build] --> B{解析 go.mod}
B --> C[向 proxy 请求 github.com/good/util@v1.0.0]
C --> D[proxy 返回篡改包]
D --> E[go.sum 校验通过<br>(哈希匹配原始发布)]
E --> F[运行时执行恶意代码]
4.4 CGO启用场景下的C库漏洞传导:libsqlite3与openssl绑定库在Go Web服务中的实际攻击面测绘
CGO桥接使Go服务直面C生态安全风险。当net/http服务启用// #cgo LDFLAGS: -lsqlite3 -lssl时,底层C库漏洞可穿透Go内存安全边界。
漏洞传导路径
- OpenSSL 1.1.1w 中的
SSL_get_shared_ciphers()空指针解引用可触发进程崩溃 - libsqlite3 3.40.1 的
fts5EvalNearTest()堆缓冲区溢出可被构造HTTP请求体远程利用
典型绑定代码片段
/*
#cgo LDFLAGS: -lsqlite3 -lssl -lcrypto
#include <sqlite3.h>
#include <openssl/ssl.h>
*/
import "C"
此声明强制链接静态C库,使Go二进制文件携带对应版本符号表;若宿主机系统未更新补丁,运行时将加载存在CVE-2023-0286(OpenSSL)或 CVE-2023-32636(SQLite)的库实例。
攻击面分布(Go Web服务典型组件)
| 组件 | 触发条件 | 传导深度 |
|---|---|---|
| HTTP TLS握手 | 自签名证书+畸形ALPN字段 | SSL层→Go net.Conn |
| SQL查询执行 | fts5全文检索恶意token |
sqlite3_exec()→http.HandlerFunc |
graph TD
A[HTTP Request] --> B{CGO-enabled Handler}
B --> C[OpenSSL SSL_read]
B --> D[sqlite3_step]
C --> E[CVE-2023-0286 Crash]
D --> F[CVE-2023-32636 RCE]
第五章:结语:构建纵深防御的Go Web安全开发生命周期
纵深防御不是堆砌工具,而是将安全能力嵌入开发流的每个触点。在某金融级API网关项目中,团队将Go安全实践拆解为五个可验证的生命周期阶段,并通过自动化门禁强制执行:
| 阶段 | 关键控制点 | Go实现示例 |
|---|---|---|
| 设计 | 威胁建模输出注入/越权检查清单 | go:generate 生成基于OpenAPI的RBAC策略校验器 |
| 编码 | 静态扫描阻断不安全函数调用 | gosec -exclude=G104,G201 ./... 集成CI |
| 构建 | 二进制签名与SBOM生成 | cosign sign --key cosign.key ./api |
| 部署 | 容器运行时SELinux策略强制启用 | securityContext: {seLinuxOptions: {type: "restricted"}} |
| 运行 | eBPF监控HTTP响应头泄露敏感信息 | bpftrace -e 'kprobe:sys_sendto /pid == 1234/ { printf("Leaked header: %s", str(args->buf)) }' |
安全左移的真实代价
某电商促销系统曾因http.Redirect未校验跳转URL导致开放重定向漏洞。修复方案并非简单添加白名单,而是将redirect.Validate()封装为模块化中间件,在main.go入口统一注册:
func NewSecurityRouter() *chi.Mux {
r := chi.NewRouter()
r.Use(redirect.Middleware()) // 自动拦截非法Location头
r.Use(csrf.Protect([]byte("secret-key")))
return r
}
该中间件在测试阶段即捕获了27处未经校验的重定向调用,其中19处来自第三方SDK封装层——证明安全控制必须穿透依赖边界。
运行时防护的不可替代性
静态检查无法覆盖动态拼接的SQL查询。在征信数据服务中,团队采用sqlmock+go-sql-driver双引擎模式:开发环境强制使用mock驱动拦截所有SELECT *语句,生产环境则通过pg_stat_statements实时告警非参数化查询。一次灰度发布中,该机制捕获到ORM层意外触发的SELECT * FROM users WHERE id = ?(本应为SELECT name, email FROM users),避免了PII数据批量泄露。
工具链协同验证
安全能力需形成闭环验证。下图展示CI/CD流水线中三重校验机制的协同关系:
flowchart LR
A[代码提交] --> B[go vet + golangci-lint]
B --> C{是否含G101/G204等高危规则?}
C -->|是| D[阻断合并]
C -->|否| E[启动DAST扫描]
E --> F[对比ZAP扫描结果与SAST标记]
F --> G[仅当两者漏洞匹配率≥95%才允许部署]
某次迭代中,SAST标记了3处os/exec.Command调用,但DAST未发现利用路径。团队回溯发现这些命令均被filepath.Clean()包裹且参数来自配置文件——此时DAST的“未发现”反而验证了输入净化的有效性,而非漏报。
人员能力的持续演进
安全生命周期最终依赖开发者认知。团队建立Go安全能力矩阵,要求所有后端工程师每季度完成至少2项实战挑战:
- 使用
pprof分析内存泄漏导致的凭证残留风险 - 通过
go:embed重构模板渲染逻辑以消除SSTI攻击面 - 在K8s准入控制器中用
opa-go实现Pod标签强制加密注解
上季度全员通过率从63%提升至91%,直接反映在生产环境零日漏洞平均修复时间从72小时缩短至4.2小时。
