Posted in

【Go安全编码TOP10】:OWASP Go版漏洞图谱与AST静态扫描实战配置

第一章:Go安全编码TOP10概览与OWASP Go版漏洞图谱演进

Go语言凭借其内存安全模型、静态链接与强类型系统,在云原生与高并发场景中广受青睐,但并不天然免疫常见Web安全风险。近年来,OWASP社区持续推动Go专属安全实践落地,2023年正式发布《OWASP Go Security Top 10》,标志着从通用Web漏洞清单向语言特化防御体系的关键演进——该图谱不再简单映射Java/Python的TOP10,而是聚焦Go生态独有风险点,如unsafe包误用、net/http中间件链中断、encoding/json反序列化绕过、goroutine泄漏导致的DoS等。

Go安全编码核心差异点

  • 内存安全≠绝对安全unsafe.Pointerreflect可绕过类型检查,需严格审计;
  • 并发模型引入新攻击面:竞态条件(race)易被利用为逻辑漏洞,go run -race应纳入CI流水线;
  • 标准库默认行为隐含风险http.ServeMux不自动处理路径遍历,template.Parse未默认启用自动HTML转义。

OWASP Go版漏洞图谱关键演进

年份 核心变化 典型新增项
2021 初版草案 A07:2021-Insecure Deserialization (Go) 强调gob/json反序列化上下文缺失风险
2023 正式发布 A04:2023-Unsafe Reflection & Unsafe Pointers 单独成类,要求所有unsafe调用必须经//go:linkname//nolint:unsafe显式标注并附安全评审记录

实战检测示例:识别危险反射模式

// ❌ 危险:动态调用无校验
func callMethod(obj interface{}, methodName string) {
    v := reflect.ValueOf(obj).MethodByName(methodName)
    if v.IsValid() {
        v.Call(nil) // 可能触发未授权方法执行
    }
}

// ✅ 安全:白名单+类型约束
type SafeCaller interface {
    AllowedMethods() map[string]bool
}
func safeCall(obj SafeCaller, methodName string) error {
    if !obj.AllowedMethods()[methodName] {
        return fmt.Errorf("method %s not allowed", methodName)
    }
    // ...后续调用逻辑
}

此代码块体现Go安全编码核心原则:显式优于隐式,约束优于放行。所有反射与unsafe操作必须伴随运行时校验与编译期注释标记,且需在CI中强制扫描//nolint:unsafe注释是否附带Jira工单ID。

第二章:Go语言核心安全风险深度解析

2.1 注入类漏洞(SQLi/OS Command/Template)的Go语义成因与AST特征识别

注入漏洞在 Go 中本质源于字符串拼接脱离类型约束,导致编译期无法校验执行语义。典型成因包括:

  • database/sql 中直接拼接 fmt.Sprintf("SELECT * FROM users WHERE id = %s", input)
  • os/exec.Command("sh", "-c", "ls "+userInput) 忽略参数隔离
  • 模板引擎中误用 template.HTML 绕过自动转义

AST关键识别模式

Go 的 ast.CallExpr 若满足以下组合即高危:

  • Fun 指向 fmt.Sprintf / exec.Command / template.New(...).Parse(...)
  • Args 包含未经 sql.Named, exec.CommandContext, 或 template.HTMLEscapeString 处理的变量
// ❌ 危险:原始输入直入SQL查询
query := "SELECT name FROM users WHERE id = " + userID // userID 来自 HTTP 参数
rows, _ := db.Query(query) // AST中:BinaryExpr + Ident → 缺失sql.Arg抽象

该代码在 AST 中表现为 *ast.BinaryExpr 连接字符串字面量与 *ast.Ident(userID),无 *ast.CallExpr 调用安全封装函数,是 SQLi 的典型 AST 指纹。

漏洞类型 AST 触发节点 安全替代方案
SQLi *ast.BinaryExpr + db.Query db.QueryRow("WHERE id = ?", id)
OS Command exec.Command("sh", "-c", ...) exec.Command("ls", path)
graph TD
    A[用户输入] --> B{是否经类型化封装?}
    B -->|否| C[AST: BinaryExpr/CallExpr with raw string]
    B -->|是| D[AST: CallExpr with sql.Named/exec.Command arg list]
    C --> E[注入风险]

2.2 不安全反序列化在Go生态中的典型载体(encoding/gob、json.Unmarshal、第三方库)及静态检测模式

数据同步机制

encoding/gob 因支持任意类型注册与二进制还原,成为高危载体:

// 危险示例:反序列化不受控的 gob 数据
var obj interface{}
err := gob.NewDecoder(r).Decode(&obj) // ❌ 无类型白名单,可触发任意类型构造函数

obj 可能为恶意构造的 net/http.Client 或含 init() 的自定义类型,导致RCE或资源耗尽。

JSON解析陷阱

json.Unmarshal 虽默认限制基础类型,但配合 json.RawMessageinterface{} 仍可绕过:

type Payload struct {
    Data json.RawMessage `json:"data"`
}
// 后续用 json.Unmarshal(Data, &target) → target 可为攻击者控制的嵌套结构

第三方库风险面

库名 风险点 检测关键词
gopkg.in/yaml.v3 Unmarshal 支持 !!python/object yaml.Unmarshal
github.com/mitchellh/mapstructure 自动类型转换触发反射调用 Decode, DecodeHook

静态检测模式

graph TD
    A[AST遍历] --> B{是否调用 Unmarshal/Decode?}
    B -->|是| C[检查参数类型是否为 interface{} 或 RawMessage]
    C --> D[检查上游数据源是否来自网络/文件]
    D --> E[标记高风险节点]

2.3 并发安全陷阱:竞态条件、sync.Mutex误用与context.Context泄漏的AST语法树定位策略

数据同步机制

常见误用:在 sync.Mutex 保护范围外读写共享字段。

var counter int
var mu sync.Mutex

func unsafeInc() {
    mu.Lock()
    defer mu.Unlock()
    counter++ // ✅ 受保护
    log.Println(counter) // ❌ 日志打印不应暴露临界区逻辑
}

log.Println 虽不修改状态,但延长锁持有时间,易引发 goroutine 阻塞;应将非临界操作移出 Lock()/Unlock() 区域。

AST定位策略

使用 go/ast 遍历函数体,识别:

  • (*ast.CallExpr).Fun 是否为 (*ast.SelectorExpr).Sel.Name == "Lock"/"Unlock"
  • (*ast.AssignStmt)(*ast.IncDecStmt) 是否位于锁区间外
节点类型 危险信号 检测依据
ast.IncDecStmt 竞态写入 *ast.CallExpr 锁调用包裹
ast.CallExpr context.WithCancel 未 defer 缺失 defer cancel() 语句
graph TD
    A[Parse Go file] --> B[Visit FuncDecl]
    B --> C{Find Lock/Unlock calls?}
    C -->|Yes| D[Extract critical section range]
    C -->|No| E[Report missing mutex]
    D --> F[Scan for unprotected inc/assign]

2.4 硬编码凭证与敏感配置泄露:Go build tag、embed.FS与环境变量注入的跨文件依赖分析实践

硬编码凭证常隐匿于多文件协作场景中,易被 go build -tagsembed.FS 无意暴露。

embed.FS 中的隐蔽泄露风险

// config/embedded.go
package config

import "embed"

//go:embed secrets/*.json
var SecretsFS embed.FS // ⚠️ 若 secrets/ 含 prod-credentials.json,build 时将全量打包

embed.FS 在编译期静态嵌入全部匹配文件,不支持按 build tag 过滤子路径;即使 secrets/ 仅用于本地测试,也会随二进制分发。

环境变量注入的跨文件耦合

// auth/provider.go
func NewAuth(cfg *Config) *Auth {
    return &Auth{
        Token: os.Getenv("API_TOKEN"), // 依赖外部注入,但 init() 可能早于 env 设置
    }
}

os.Getenv 调用时机与 init() 顺序强相关,若 auth/provider.go 被其他包提前导入,可能导致空值解引用。

安全实践对比

方式 静态扫描友好 构建隔离性 运行时可控性
硬编码字符串
embed.FS + JSON ❌(需解析) ✅(延迟读取)
os.Getenv
graph TD
    A[main.go] --> B[config/loader.go]
    B --> C[auth/provider.go]
    C --> D[os.Getenv]
    B --> E[embed.FS]
    E --> F[secrets/dev.json]
    F --> G[误入生产镜像]

2.5 TLS/SSL配置缺陷:crypto/tls包中不安全默认值、证书验证绕过及AST规则定制化扫描

Go 标准库 crypto/tls 默认启用 TLS 1.0–1.2,且 InsecureSkipVerify: true 常被误用于开发调试,却意外流入生产环境。

常见危险模式

  • 直接禁用证书校验(Config.InsecureSkipVerify = true
  • 未设置 ServerName 导致 SNI 缺失,引发证书匹配失败
  • 忽略 MinVersion,遗留弱协议(如 TLS 1.0)

危险代码示例

// ❌ 不安全:跳过证书验证 + 无 SNI + TLS 1.0 兼容
conf := &tls.Config{
    InsecureSkipVerify: true, // 绕过 CA 验证 → 中间人攻击面
    ServerName:         "",   // 空 ServerName → 无法校验 CN/SAN
}

InsecureSkipVerify=true 使客户端完全忽略证书链有效性、域名匹配与签名验证;空 ServerName 导致 VerifyPeerCertificate 无法执行 SAN 检查,二者叠加构成高危组合。

AST 扫描关键规则维度

规则类型 检测目标 严重等级
TLS_SKIP_VERIFY InsecureSkipVerify == true CRITICAL
MISSING_SERVER_NAME ServerName == ""VerifyPeerCertificate 未重写 HIGH
WEAK_TLS_VERSION MinVersion < tls.VersionTLS12 MEDIUM
graph TD
    A[AST 解析 Go 源码] --> B{发现 tls.Config 字面量}
    B --> C[提取 InsecureSkipVerify 值]
    B --> D[提取 ServerName 字符串]
    B --> E[提取 MinVersion 数值]
    C --> F[触发 CRITICAL 规则]
    D --> G[触发 HIGH 规则]
    E --> H[触发 MEDIUM 规则]

第三章:Go AST抽象语法树原理与安全扫描器构建基础

3.1 Go parser与ast.Package的结构解析:从源码到语法树的完整映射路径

Go 的 go/parser 包将 .go 源文件转化为抽象语法树(AST),核心入口是 parser.ParseFile(),其返回值即为 *ast.File;而 ast.Package 是多个 *ast.File 的逻辑集合,代表一个完整包。

解析流程概览

graph TD
    A[源码字节流] --> B[scanner.Scanner]
    B --> C[parser.Parser]
    C --> D[*ast.File]
    D --> E[ast.Package]

ast.Package 关键字段

字段名 类型 说明
Name string 包名(如 "main"
Files map[string]*ast.File 文件路径 → AST 根节点映射
Imports []*ast.ImportSpec 所有导入声明(含位置信息)

典型解析调用示例

fset := token.NewFileSet()
pkg, err := parser.ParseDir(fset, "./cmd", nil, parser.ParseComments)
// fset: 记录每个节点的源码位置(行/列/偏移)
// "./cmd": 待解析目录路径
// nil: 过滤函数(可指定只解析特定文件)
// parser.ParseComments: 启用注释节点捕获

该调用遍历目录下所有 .go 文件,构建统一 ast.Package,每个 *ast.FileName 字段对应 ast.Ident 节点,其 NamePos 精确定位包声明位置。

3.2 基于go/ast与go/types的安全规则编写范式:节点遍历、类型推断与上下文感知

安全规则需同时理解语法结构与语义含义。go/ast 提供树形遍历能力,go/types 则补全类型信息——二者协同构成静态分析基石。

节点遍历:从 AST 到敏感操作识别

使用 ast.Inspect 遍历函数体,定位 *ast.CallExpr 节点:

ast.Inspect(file, func(n ast.Node) bool {
    call, ok := n.(*ast.CallExpr)
    if !ok || call.Fun == nil { return true }
    ident, isIdent := call.Fun.(*ast.Ident)
    if isIdent && ident.Name == "os.Open" { // 检测不安全文件打开
        report("unsafe os.Open usage", call.Pos())
    }
    return true
})

ast.Inspect 深度优先遍历,return true 继续,false 中止子树;call.Pos() 提供精确位置用于报告。

类型推断:区分 io.Readnet/http.Get

表达式类型 types.Info.Types[call].Type 推断结果 安全含义
http.Get(...) *http.Response 可能含未关闭响应体
os.Open(...) *os.File 需显式 Close()

上下文感知:结合作用域与调用栈

graph TD
    A[AST遍历发现CallExpr] --> B{类型检查}
    B -->|*http.Response| C[检查defer/Close调用]
    B -->|*os.File| D[检查err != nil后是否Close]

规则有效性依赖三者闭环:语法结构驱动遍历,类型系统校验语义,作用域信息约束上下文边界。

3.3 Go SSA中间表示在复杂控制流漏洞(如逻辑绕过、权限校验缺失)检测中的增强应用

Go 的 SSA(Static Single Assignment)形式将函数转化为标准化的三地址码,使控制流与数据流显式分离,为深度语义分析提供坚实基础。

控制流图重构与敏感路径识别

SSA 构建的 CFG 可精准定位 if/goto/panic 节点,并标记所有可能跳过认证检查的异常边:

// 示例:权限校验缺失的 SSA 形式片段(简化)
t1 = load ptr @user
t2 = call is_admin(t1)          // 权限检查调用
br t2, L_auth_ok, L_bypass      // 关键分支:若 t2 为 false 则跳入 L_bypass
L_bypass:
  ret @sensitive_data           // 危险:未校验即返回敏感数据

逻辑分析br 指令暴露了未经验证的数据出口;t2 是唯一校验信号,但其真值分支 L_auth_ok 未被强制执行。参数 t2 来自 is_admin 返回值,SSA 保证其定义唯一,便于跨基本块污点追踪。

检测规则增强机制

  • 基于 SSA 的 φ 节点分析跨路径变量收敛性
  • 结合 @security_check 函数调用图谱识别校验锚点
  • ret/store 指令反向追溯至最近校验节点距离
漏洞模式 SSA 特征信号 检测置信度
权限校验缺失 br 后无校验调用且存在敏感出口
逻辑绕过(短路) 多重 and 中首个条件恒真导致跳过 中高
graph TD
  A[Entry] --> B{is_authenticated?}
  B -- true --> C[is_admin?]
  B -- false --> D[ret sensitive_data]  %% 绕过路径
  C -- false --> D
  C -- true --> E[allow_access]

第四章:Go安全静态扫描工具链实战配置与深度集成

4.1 golangci-lint安全插件体系搭建:集成govet、staticcheck与自定义AST规则的CI/CD流水线配置

核心插件选型与能力边界

  • govet:检测死代码、未使用的变量、反射 misuse 等运行时隐患;
  • staticcheck:提供更严格的静态分析(如 SA1019 弃用API调用、SA9003 不安全的并发写入);
  • 自定义 AST 规则:基于 golang.org/x/tools/go/analysis 实现敏感函数(如 http.ListenAndServe 未启用 TLS 的硬编码检查)。

CI 配置示例(.golangci.yml

run:
  timeout: 5m
  skip-dirs: ["vendor", "testutil"]
linters-settings:
  govet:
    check-shadowing: true  # 启用作用域遮蔽检测
  staticcheck:
    checks: ["all", "-ST1000"]  # 启用全部检查,排除模糊建议
  custom-ast-rule:
    enabled: true
    severity: error

该配置使 govet 捕获变量遮蔽风险,staticcheck 覆盖 API 安全性与并发模型缺陷,自定义规则通过 AST 遍历节点精准拦截明文 HTTP 服务启动——三者协同形成纵深防御层。

流水线集成流程

graph TD
  A[代码提交] --> B[GitLab CI 触发]
  B --> C[golangci-lint 执行]
  C --> D{发现高危问题?}
  D -->|是| E[阻断构建并标记 PR]
  D -->|否| F[继续测试与部署]

4.2 Semgrep for Go:基于语法模式匹配的TOP10漏洞POC规则编写与误报抑制技巧

规则编写核心:AST驱动的精准定位

Semgrep 对 Go 的支持依赖 go 语言解析器生成的 AST。编写规则时,应优先匹配语义节点(如 CallExprAssignStmt),而非字符串正则。

典型漏洞:硬编码凭证检测

rules:
- id: go-hardcoded-credentials
  patterns:
    - pattern: '$X := "$SECRET"'
    - pattern-not: '$X := os.Getenv(...)'
  message: "Hardcoded credential detected in assignment"
  languages: [go]
  severity: ERROR

逻辑分析:$X := "$SECRET" 匹配字面量赋值;pattern-not 排除通过 os.Getenv 动态加载的合法场景,显著降低误报。

误报抑制三原则

  • 使用 pattern-not-inside 避免测试文件触发
  • 限定 metavariable-regex 约束 $SECRET 仅匹配 Base64/Hex 格式密钥
  • 结合 taint-mode: sources-sinks 追踪敏感数据流
技巧 作用 示例
pattern-not-inside 排除测试/示例代码 func Test... { ... }
metavariable-regex 约束变量内容格式 secret: "^[a-zA-Z0-9+/]{32,}$"
graph TD
    A[AST Parse] --> B[Pattern Match]
    B --> C{pattern-not applied?}
    C -->|Yes| D[Filter False Positive]
    C -->|No| E[Report Alert]

4.3 CodeQL for Go:构建精准污点追踪查询(Source→Sink→Sanitizer),覆盖HTTP Handler与数据库交互链路

污点流建模核心三元组

CodeQL for Go 中需明确定义:

  • Sourcehttp.HandlerFunc 参数 r *http.Requestr.URL.Query()r.FormValue() 等用户可控输入
  • Sinkdatabase/sqldb.Query()db.Exec() 等执行点
  • Sanitizersql.EscapeString()、参数化查询(? 占位符 + args...

关键查询片段(带上下文过滤)

import go
import semmle.go.dataflow.TaintTracking
import semmle.go.security.DataFlow::TaintTracking::DataFlowConfiguration

class GoHttpTaintConfig extends DataFlow::Configuration {
  override predicate isSource(DataFlow::Node source) {
    exists(http::HandlerFunc hf |
      source.asExpr() = hf.getParameter(0).getAChild*().getAnArgument()
      and source.asExpr().getEnclosingFunction() instanceof http::HandlerFunc
      and (source.asExpr().toString().matches("%URL.Query%") or 
           source.asExpr().toString().matches("%FormValue%"))
    )
  }
  override predicate isSink(DataFlow::Node sink) {
    exists(sql::DB db | sink.asExpr().getCall("Query").getReceiver() = db)
  }
}

该配置限定污点仅从 HTTP 处理器入参出发,排除中间件封装调用;isSink 精确匹配 *sql.DB.Query 调用,避免误报 sql.Open 等非执行点。

典型防护绕过模式识别

模式类型 示例代码 CodeQL 检测逻辑
字符串拼接SQL "SELECT * FROM users WHERE id=" + id | 匹配+` 连接且右侧为污点源
未参数化Prepare db.Prepare("UPDATE t SET v=" + v) 检查 Prepare 字符串字面量含变量插值

数据流路径可视化

graph TD
  A[http.Request.FormValue] --> B[User Input String]
  B --> C[Unsafe SQL Concatenation]
  C --> D[db.Query Raw String]
  E[sql.Named] -->|Sanitizer| B
  F[db.QueryContext _, ? , args...] -->|Sanitizer| B

4.4 自研轻量级AST扫描器开发:使用go/ast+golang.org/x/tools/go/analysis实现可扩展安全检查框架

核心架构设计

基于 golang.org/x/tools/go/analysis 构建插件化检查框架,每个规则封装为独立 analysis.Analyzer,共享统一的 pass 生命周期。

关键代码示例

var UnsafeExecAnalyzer = &analysis.Analyzer{
    Name: "unsafeexec",
    Doc:  "detect os/exec.Command with untrusted input",
    Run:  runUnsafeExec,
}

func runUnsafeExec(pass *analysis.Pass) (interface{}, error) {
    for _, file := range pass.Files {
        ast.Inspect(file, func(n ast.Node) bool {
            if call, ok := n.(*ast.CallExpr); ok {
                if isExecCommand(call) {
                    pass.Reportf(call.Pos(), "dangerous exec with untrusted args")
                }
            }
            return true
        })
    }
    return nil, nil
}

逻辑分析Run 函数接收 *analysis.Pass,遍历 AST 节点;ast.Inspect 深度优先遍历,isExecCommand 判定调用是否为 os/exec.Commandpass.Reportf 统一报告位置与消息,支持跨文件分析上下文。

扩展能力对比

特性 go/ast 原生扫描 x/tools/go/analysis
类型信息支持 ✅(通过 pass.TypesInfo
多文件依赖分析
并发安全检查 ⚠️需手动同步 ✅(框架内置)

规则注册流程

graph TD
    A[定义 Analyzer] --> B[实现 Run 方法]
    B --> C[注入 Checker 集合]
    C --> D[go vet -tool=xxx 启动]

第五章:面向生产环境的Go安全编码治理闭环

在某金融级支付平台的Go服务迭代中,团队曾因未校验http.Request.URL.RawQuery直接拼接日志字段,导致攻击者注入恶意URL片段触发SSRF,最终渗透至内网风控数据库。这一事件成为推动构建自动化、可度量、可持续的安全编码治理闭环的直接动因。

静态分析与CI/CD深度集成

采用gosec与自定义go vet检查器组合扫描,嵌入GitLab CI流水线的pre-merge阶段。当检测到crypto/md5unsafe包被引入时,流水线自动阻断合并,并附带修复指引链接至内部《加密规范白皮书》。扫描结果实时同步至SonarQube,关键漏洞(如CWE-79、CWE-89)标记为Blocker级,触发Jira自动创建高优工单。

运行时防护与动态策略注入

在Kubernetes集群中部署eBPF驱动的go-runtime-probe,实时捕获net/http.(*Server).Serve调用栈及database/sql参数绑定行为。当发现SQL查询字符串含UNION SELECT且未使用sql.Named参数化时,立即熔断该请求并上报至Falco告警中心,同时向Jaeger注入security_event: true标签便于溯源。

治理环节 工具链 响应SLA 误报率
代码提交前 golangci-lint + 自研rule-set 1.2%
构建阶段 gosec + Semgrep规则集 0.7%
运行时监控 eBPF probe + OpenTelemetry 0.3%

安全配置基线强制落地

通过go mod vendor锁定golang.org/x/crypto至v0.23.0(含CVE-2023-39325修复),并在Dockerfile中声明GO111MODULE=onGOSUMDB=sum.golang.org。所有容器镜像构建后执行trivy filesystem --security-check vuln,config ./,拒绝含CVE-2022-27191weak-cipher-suite配置的镜像推送至Harbor仓库。

// 生产环境HTTP服务器强制启用安全头
func NewSecureServer(addr string) *http.Server {
    return &http.Server{
        Addr: addr,
        Handler: secureMiddleware(http.HandlerFunc(handlePayment)),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  30 * time.Second,
    }
}

func secureMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        next.ServeHTTP(w, r)
    })
}

开发者反馈与知识沉淀机制

每月生成《Go安全缺陷热力图》,基于Git blame统计各模块time.Now().Unix()硬编码、rand.Int()未seed等反模式出现频次,TOP3问题自动推送至对应Team Slack频道,并关联Confluence中已验证的修复方案快照。2024年Q2数据显示,crypto/rand.Read误用率下降82%,os/exec.Command参数拼接漏洞归零。

flowchart LR
    A[开发者提交PR] --> B{gosec扫描}
    B -->|存在CWE-78| C[阻断合并+生成修复建议]
    B -->|通过| D[Trivy镜像扫描]
    D -->|发现CVE| E[拒绝镜像推送]
    D -->|通过| F[部署至预发环境]
    F --> G[eBPF运行时监控]
    G -->|检测到异常SQL| H[熔断+告警+Trace注入]
    G -->|正常| I[灰度发布]

跨团队协同治理看板

在Grafana中构建“安全健康度”看板,聚合gosec漏洞密度(per KLOC)、trivy镜像风险指数、eBPF拦截事件数三维度数据,按服务网格Namespace分组。当支付核心服务payment-svc的SQL注入拦截率周环比上升300%,自动触发SRE与开发负责人联合复盘会议。

专攻高并发场景,挑战百万连接与低延迟极限。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注