第一章:Go语言安全编码红线总览与CNVD-2022漏洞态势分析
Go语言凭借其内存安全模型、静态类型系统和简洁的并发原语,常被误认为“天然免疫”常见安全风险。然而,CNVD-2022年度统计数据显示,全年收录的Go生态相关漏洞达147个,其中68%源于编码实践偏差,而非语言机制缺陷。高频风险类型包括:不安全的反序列化(如gob/json.Unmarshal未校验输入)、HTTP头注入(Header.Set直接拼接用户数据)、路径遍历(filepath.Join未清理..)、以及竞态敏感逻辑中滥用sync/atomic绕过锁保护。
关键安全红线清单
- 禁止将未经验证的用户输入直接传入
template.Parse或html/template.New;必须使用html.EscapeString预处理或依赖模板自动转义机制 - 禁止在
http.HandlerFunc中调用log.Printf("%s", r.URL.Query().Get("debug"))——格式化字符串漏洞可导致任意日志注入 - 禁止使用
os/exec.Command("sh", "-c", user_input)执行动态命令;应改用exec.Command("ls", path)并显式拆分参数
CNVD-2022典型漏洞复现示例
以下代码片段触发CNVD-2022-10392(Go标准库net/http重定向头注入):
func unsafeRedirect(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("to")
// ❌ 危险:未校验target是否为绝对URL或含换行符
w.Header().Set("Location", target)
w.WriteHeader(http.StatusFound)
}
修复方式:使用net/url.ParseRequestURI验证并拒绝非HTTP(S)协议及含CRLF的输入:
if u, err := url.ParseRequestURI(target); err != nil || u.Scheme == "" || u.Scheme != "https" {
http.Error(w, "Invalid redirect", http.StatusBadRequest)
return
}
// ✅ 仅允许同域HTTPS绝对路径
w.Header().Set("Location", u.String())
2022年Go漏洞分布特征(CNVD公开数据)
| 风险类型 | 占比 | 典型CVE示例 |
|---|---|---|
| 输入验证缺失 | 31% | CVE-2022-23806 |
| 并发竞态条件 | 22% | CVE-2022-27192 |
| 依赖组件漏洞 | 28% | CVE-2022-24921 |
| 配置错误 | 19% | CVE-2022-30629 |
第二章:SQL注入漏洞的Go原生防御体系构建
2.1 Go数据库驱动原理与sql.RawBytes风险解构
Go 的 database/sql 包采用抽象驱动层(sql.Driver)+ 具体实现(如 mysql.MySQLDriver)的双层架构,通过 sql.Conn 和 driver.Rows 接口桥接底层协议。
数据读取生命周期
- 驱动将网络字节流解包为
[]driver.Value sql.Rows.Scan()调用driver.Value.ConvertValue()类型转换sql.RawBytes是零拷贝引用:直接指向底层[]byte缓冲区,不复制数据
sql.RawBytes 的典型误用
var raw sql.RawBytes
err := row.Scan(&raw)
if err != nil { return }
s := string(raw) // ⚠️ 危险:raw 可能被下一行 row.Next() 复用覆盖
逻辑分析:
sql.RawBytes本质是[]byte切片,其底层数组由驱动缓冲池管理;row.Next()会重置该缓冲区。若未及时深拷贝(如append([]byte{}, raw...)),将引发脏读或 panic。
| 风险类型 | 触发条件 | 后果 |
|---|---|---|
| 内存越界读取 | RawBytes 超出缓冲区长度 |
panic: runtime error |
| 数据静默污染 | 多次 Scan() 共享同一缓冲 |
后续行内容覆盖前值 |
graph TD
A[Query 执行] --> B[驱动分配固定缓冲区]
B --> C[Row 1: RawBytes 指向缓冲区起始]
C --> D[Row 2: 复用同一缓冲区]
D --> E[Row 1 的 RawBytes 变为无效引用]
2.2 database/sql预处理机制深度剖析与参数化查询强制实践
预处理的本质:客户端缓存 + 服务端编译
database/sql 的 Prepare() 并非仅缓存 SQL 字符串,而是将参数占位符(? 或 $1)交由底层驱动与数据库协商编译为执行计划,后续 Exec()/Query() 复用该计划,规避语法解析与权限校验开销。
安全强制实践:禁止字符串拼接
// ❌ 危险:SQL注入温床
query := "SELECT name FROM users WHERE id = " + userID
// ✅ 强制:参数化绑定
stmt, _ := db.Prepare("SELECT name FROM users WHERE id = ?")
defer stmt.Close()
row := stmt.QueryRow(42) // 参数类型自动推导并转义
逻辑分析:
QueryRow(42)将整数42作为二进制参数传入,驱动不经过字符串序列化,彻底阻断' OR 1=1 --类注入。参数类型由 Go 类型系统静态约束,避免隐式转换漏洞。
预处理生命周期对比
| 场景 | 是否复用执行计划 | 连接绑定关系 |
|---|---|---|
db.Prepare() |
✅ | 跨连接共享 |
tx.Prepare() |
✅ | 绑定至事务连接 |
stmt.Close() |
❌(释放服务端资源) | — |
graph TD
A[调用 db.Prepare] --> B[驱动发送 PREPARE 指令]
B --> C[数据库返回 statement_id]
C --> D[Go 内部缓存 stmt 结构体]
D --> E[后续 Query/Exec 仅传 statement_id + 参数二进制流]
2.3 GORM v1/v2安全配置陷阱与SafeScan防注入模式复现
GORM v1 默认启用 sql.Null* 类型自动映射,v2 则默认禁用 AllowGlobalUpdate 但遗留 Select("*") 易被构造恶意字段。
常见注入触发点
db.Where("name = ?", name).First(&u)✅ 安全(参数化)db.Where("name = " + name).First(&u)❌ 危险(字符串拼接)db.Select(columns).Find(&users)⚠️ 若columns = "id, name, (SELECT password FROM users LIMIT 1)"则触发子查询注入
SafeScan 防注入模式复现
// 启用 SafeScan:仅允许白名单字段 + 强制结构体绑定
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:name"`
}
var users []User
db.SafeSelect("id,name").Find(&users) // 自动过滤非法字段/表达式
SafeSelect 内部校验字段名是否存在于结构体标签中,拒绝 *、函数调用、嵌套 SQL;column 标签为唯一可信源。
| 配置项 | GORM v1 默认 | GORM v2 默认 | 安全建议 |
|---|---|---|---|
| AllowGlobalUpdate | true | false | 保持 false |
| PrepareStmt | false | true | 开启防预编译绕过 |
| UnsafeSelect | false | false | 禁用以规避列注入 |
graph TD
A[用户输入 columns] --> B{字段白名单校验}
B -->|通过| C[生成安全 SELECT]
B -->|失败| D[panic: invalid column]
2.4 自定义SQL构建器的AST校验方案与go-sqlmock安全测试闭环
AST校验核心流程
解析SQL字符串为抽象语法树(AST)后,校验节点合法性:禁止DROP TABLE、;多语句、未绑定参数等高危模式。
func ValidateAST(node ast.Node) error {
switch n := node.(type) {
case *ast.DropTableStmt:
return errors.New("DROP TABLE prohibited in builder context")
case *ast.SelectStmt:
if hasRawSubquery(n) { // 检查嵌套未参数化子查询
return errors.New("raw subquery violates parameterization policy")
}
}
return nil
}
ValidateAST递归遍历AST节点:*ast.DropTableStmt触发硬性拒绝;hasRawSubquery检测SELECT (SELECT ...)中内层未使用?占位符的场景,防止SQL注入逃逸。
安全测试闭环设计
| 阶段 | 工具 | 目标 |
|---|---|---|
| 构建时 | custom AST walker | 拦截非法语法结构 |
| 单元测试 | go-sqlmock | 验证生成SQL与预期完全匹配 |
| 运行时 | sqlmock.ExpectQuery | 拦截未声明的SQL执行 |
graph TD
A[Builder.Build] --> B[AST Parse]
B --> C{ValidateAST}
C -->|OK| D[Generate SQL]
C -->|Fail| E[panic with violation]
D --> F[sqlmock.ExpectQuery]
F --> G[Run Test]
2.5 基于context.Context的查询超时+取消+审计日志三位一体防护链
在高并发数据访问场景中,单次数据库查询需同时满足可中断、有时限、可追溯三重约束。context.Context 是实现该防护链的核心载体。
三位一体协同机制
- 超时控制:
context.WithTimeout()设定硬性截止时间 - 主动取消:外部信号触发
ctx.Cancel()中断阻塞操作 - 审计埋点:
ctx.Value()携带请求ID、操作类型等元信息
关键代码示例
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ctx = context.WithValue(ctx, "req_id", "req_7a2f")
ctx = context.WithValue(ctx, "op_type", "SELECT_USER")
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", userID)
逻辑分析:
QueryContext将ctx透传至驱动层;超时或调用cancel()时,PostgreSQL 驱动自动发送CancelRequest协议包终止后端查询;ctx.Value()提供无侵入式审计上下文,避免在业务参数中显式传递元数据。
| 组件 | 作用 | 触发条件 |
|---|---|---|
WithTimeout |
设置查询生命周期上限 | 启动时预设 |
Cancel() |
异步终止进行中的查询 | 外部调用(如HTTP超时) |
WithValue |
注入审计字段,供中间件提取 | 请求初始化阶段 |
graph TD
A[HTTP Handler] --> B[WithTimeout/WithValue]
B --> C[DB QueryContext]
C --> D{执行中?}
D -->|是| E[响应返回]
D -->|否| F[Cancel → 驱动中断 → 审计日志落盘]
第三章:反序列化漏洞的Go语言免疫路径
3.1 encoding/json与encoding/xml反射机制中的unsafe.UnsafePointer隐患溯源
encoding/json 和 encoding/xml 在结构体字段反射遍历时,为提升性能会绕过类型安全检查,直接通过 unsafe.Pointer 计算字段偏移。这一优化在 Go 1.18+ 泛型与内存布局变更后埋下隐患。
字段对齐与指针偏移错位
type User struct {
ID int64
Name string // string header 占 16B(ptr + len),但 runtime.structfield.Offset 可能未对齐
}
逻辑分析:
reflect.StructField.Offset返回的是字节偏移,但unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + field.Offset)若未考虑字段对齐边界(如string在 32 位系统中可能因 padding 偏移异常),将导致越界读取。参数field.Offset不保证内存安全,仅反映编译期布局快照。
典型触发场景
- 结构体含
sync.Mutex等 no-copy 字段时,反射写入引发竞态; - CGO 交互中
C.struct_x转 Go struct 时unsafe.Pointer强转丢失生命周期约束。
| 风险类型 | 触发条件 | 检测方式 |
|---|---|---|
| 内存越界读 | 字段偏移 + size > struct size | -gcflags="-d=checkptr" |
| 悬垂指针写入 | 反序列化到栈分配临时结构体 | go run -race |
graph TD
A[UnmarshalJSON] --> B[reflect.Value.Set]
B --> C[unsafe.Pointer + Offset]
C --> D{对齐校验?}
D -->|否| E[UB: 读写非法地址]
D -->|是| F[安全赋值]
3.2 json.RawMessage零拷贝防御策略与自定义UnmarshalJSON白名单校验
json.RawMessage 是 Go 标准库中实现延迟解析与零拷贝的关键类型,它仅保存原始 JSON 字节切片引用,避免重复解码开销。
防御恶意嵌套与超长 payload
使用 RawMessage 可将未知结构字段暂存为字节流,配合长度预检与深度限制:
type Event struct {
ID string `json:"id"`
Payload json.RawMessage `json:"payload"`
}
func (e *Event) UnmarshalJSON(data []byte) error {
type Alias Event // 防止递归调用
aux := &struct {
Payload json.RawMessage `json:"payload"`
*Alias
}{Alias: (*Alias)(e)}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
// 白名单校验:仅允许预定义键
allowedKeys := map[string]struct{}{"user_id": {}, "action": {}, "ts": {}}
var m map[string]json.RawMessage
if err := json.Unmarshal(aux.Payload, &m); err != nil {
return errors.New("invalid payload format")
}
for key := range m {
if _, ok := allowedKeys[key]; !ok {
return fmt.Errorf("disallowed field: %s", key)
}
}
e.Payload = aux.Payload
return nil
}
逻辑分析:先通过匿名嵌套结构绕过
UnmarshalJSON递归陷阱;再对Payload做二次解析为map[string]RawMessage,逐键比对白名单。aux.Payload仍为原始字节引用,全程无内存拷贝。
白名单校验维度对比
| 维度 | 静态结构体绑定 | RawMessage + 白名单 |
|---|---|---|
| 内存开销 | 高(全量解码) | 极低(仅键名解析) |
| 扩展性 | 编译期锁定 | 运行时动态可控 |
| 安全边界 | 依赖字段标签 | 显式键名过滤 |
graph TD
A[收到JSON] --> B{Payload长度 ≤ 1MB?}
B -->|否| C[拒绝]
B -->|是| D[解析顶层字段]
D --> E[提取RawMessage]
E --> F[键名白名单校验]
F -->|通过| G[业务逻辑处理]
F -->|失败| H[返回400 Bad Request]
3.3 YAML/ TOML解析器的构造函数劫持漏洞复现与gopkg.in/yaml.v3安全加固
漏洞成因:未限制!!python/object/apply等危险标签
YAML v2 默认启用非安全解析器,允许反序列化任意类型构造函数,攻击者可注入恶意指令:
# payload.yaml
!!python/object/apply:os.system ["id"]
复现关键代码(gopkg.in/yaml.v2)
import "gopkg.in/yaml.v2"
// ❌ 危险用法:未禁用构造器
var data interface{}
yaml.Unmarshal([]byte(yamlPayload), &data) // 触发系统命令执行
Unmarshal在 v2 中默认启用unsafe解析器;yamlPayload含!!python/*标签时,会反射调用目标构造器,绕过类型约束。
安全迁移至 yaml.v3
| v2 风险行为 | v3 安全替代 |
|---|---|
yaml.Unmarshal() |
yaml.UnmarshalWithOptions() |
| 无类型白名单 | 强制启用 DisallowUnknownFields() + UseStrict() |
加固后的安全解析流程
import "gopkg.in/yaml.v3"
dec := yaml.NewDecoder(strings.NewReader(yamlPayload))
dec.KnownFields(true) // 禁止未知字段
var data map[string]interface{}
if err := dec.Decode(&data); err != nil {
log.Fatal(err) // 构造函数标签被直接拒绝
}
KnownFields(true)拦截所有未声明的 YAML 节点(含!!标签),Decode不再触发反射构造,彻底阻断劫持链。
第四章:竞态条件漏洞的Go原生治理范式
4.1 sync.Mutex与RWMutex在HTTP Handler中的误用场景与pprof -mutexprofile实证分析
数据同步机制
HTTP handler中常因共享状态(如计数器、缓存映射)引入锁,但易忽略读写频率差异:
sync.Mutex全局互斥,高并发读场景下成为性能瓶颈;sync.RWMutex本应提升读吞吐,但若混用Lock()/RLock()或存在写饥饿,反而加剧阻塞。
典型误用代码
var mu sync.RWMutex
var hits = make(map[string]int)
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock() // ❌ 错误:读操作不应使用写锁
hits[r.URL.Path]++
mu.Unlock()
fmt.Fprintf(w, "OK")
}
逻辑分析:每次请求都独占写锁,完全丧失RWMutex的并发读优势;hits 更新频次低,应分离读写路径,或改用原子操作+分片。
pprof实证验证
启用 GODEBUG=muxprofile=1 并采集 -mutexprofile=mutex.out 后,可定位争用热点。关键指标: |
Metric | Healthy | Problematic |
|---|---|---|---|
| mutex contention | > 10ms per lock | ||
| avg wait time | ~0.1ms | > 5ms (indicates starvation) |
优化路径
- 读多写少 →
RWMutex+RLock()for GET,Lock()only for PUT/DELETE; - 高频计数 →
atomic.Int64替代 map + mutex; - 复杂状态 → 使用
sync.Map或分片锁降低粒度。
4.2 atomic.Value零锁读优化与unsafe.Pointer类型逃逸检测实战
数据同步机制
atomic.Value 通过内部 ifaceWords 结构实现类型安全的原子读写,读操作完全无锁,写操作仅需一次 Store 原子赋值。
var config atomic.Value
config.Store(&Config{Timeout: 30, Retries: 3}) // 写入指针,非值拷贝
// 读取——零开销,无 mutex、无 CAS 循环
cfg := config.Load().(*Config) // 类型断言必须精准,否则 panic
逻辑分析:
Store将接口值的data字段(即*Config地址)原子写入;Load直接返回内存地址,避免复制大结构体。参数*Config必须保持生命周期稳定,否则引发 use-after-free。
逃逸检测关键点
unsafe.Pointer 转换会抑制编译器逃逸分析,需显式校验:
go build -gcflags="-m -m" main.go
| 检测项 | 安全写法 | 危险写法 |
|---|---|---|
| 指针生命周期 | 在 goroutine 外分配并持久化 | 在栈上创建后转 unsafe.Pointer |
| 类型一致性 | (*T)(unsafe.Pointer(&x)) |
(*T)(unsafe.Pointer(uintptr(0))) |
graph TD
A[定义 Config 实例] --> B[atomic.Value.Store]
B --> C[goroutine 并发 Load]
C --> D[直接解引用 *Config]
D --> E[零锁读取成功]
4.3 context.WithCancel传播与goroutine泄漏引发的逻辑竞态复现与go test -race全覆盖验证
竞态复现场景
以下代码模拟未正确传播 cancel 的典型泄漏:
func startWorker(ctx context.Context, id int) {
go func() {
select {
case <-time.After(2 * time.Second):
fmt.Printf("worker %d done\n", id)
case <-ctx.Done(): // ✅ 正确监听
fmt.Printf("worker %d cancelled\n", id)
}
}()
}
func badExample() {
ctx, cancel := context.WithCancel(context.Background())
startWorker(ctx, 1)
cancel() // 期望终止 worker,但无同步等待 → goroutine 泄漏 + 潜在竞态
}
startWorker启动后未与cancel()建立同步约束;若cancel()后立即函数返回,workergoroutine 仍在运行并可能访问已释放变量(如闭包中临时对象),触发 data race。
go test -race 验证策略
| 检测项 | 是否覆盖 | 说明 |
|---|---|---|
| goroutine 生命周期 | ✅ | -race 捕获未结束 goroutine 写操作 |
| context 取消传播链 | ✅ | 跨 goroutine 的 ctx.Done() 监听需显式同步 |
| 变量读写时序 | ✅ | 自动标记非原子共享变量的并发访问 |
修复关键点
- 使用
sync.WaitGroup等待 worker 结束 - 或改用
context.WithTimeout+ 显式<-ctx.Done()返回路径
graph TD
A[main goroutine] -->|ctx, cancel| B[worker goroutine]
A -->|cancel()| C[ctx.Done() closed]
B -->|select ← ctx.Done()| D[安全退出]
B -.->|未监听/未等待| E[泄漏 + 竞态]
4.4 Go 1.21引入的sync.OnceValues与atomic.Int64.CompareAndSwap在状态机并发控制中的安全落地
数据同步机制
sync.OnceValues 提供线程安全的惰性多值初始化能力,避免重复计算与竞态;而 atomic.Int64.CompareAndSwap 则用于原子切换状态机阶段(如 Pending → Running → Done)。
状态跃迁保障
type StateMachine struct {
state atomic.Int64
once sync.OnceValues
}
func (sm *StateMachine) Start() (result []byte, err error) {
// 原子检查并跃迁:仅当当前为0(Idle)才设为1(Running)
if !sm.state.CompareAndSwap(0, 1) {
return nil, errors.New("state machine already started")
}
// 惰性加载配置与资源,确保仅执行一次且返回完整结果
result, err, _ = sm.once.Do(func() (any, error) {
return loadConfig(), nil
})
return result.([]byte), err
}
CompareAndSwap(0, 1) 保证状态跃迁不可重入;OnceValues.Do 返回 (value, error, loaded) 三元组,天然支持带错误的初始化语义。
对比选型
| 特性 | sync.Once |
sync.OnceValues |
atomic.CompareAndSwap |
|---|---|---|---|
| 返回值支持 | ❌(仅 void) | ✅(任意类型+error) | ✅(bool 控制流) |
| 状态建模能力 | 弱 | 中 | 强(显式状态编码) |
graph TD
A[Idle] -->|CompareAndSwap 0→1| B[Running]
B -->|OnceValues.Do| C[Loaded Result]
C -->|CompareAndSwap 1→2| D[Done]
第五章:从CNVD通报到生产级Go安全编码规范演进
CNVD-2023-12874漏洞复现与根因分析
2023年11月,CNVD通报某国产云平台API网关组件存在unsafe.Pointer误用导致的内存越界读取(CNVD-2023-12874)。我们基于其公开PoC构建最小复现场景:在bytes.Buffer扩容逻辑中,通过reflect.SliceHeader强制转换[]byte底层数组指针后执行copy(),当底层cap未同步更新时触发UB。实际审计发现,该问题源于开发者为绕过go vet检查而刻意屏蔽-gcflags="-d=checkptr"。
Go 1.21+ unsafe使用白名单机制落地实践
团队在CI流水线中集成以下编译约束策略:
# 在build.sh中强制启用安全检查
go build -gcflags="-d=checkptr" -ldflags="-s -w" ./cmd/gateway
# 同时扫描unsafe包调用链
go list -f '{{if .Imports}} {{.ImportPath}}: {{.Imports}}{{end}}' ./... | grep unsafe
对确需使用的场景(如高性能序列化),建立unsafe/whitelist.go文件,仅允许unsafe.Offsetof、unsafe.Sizeof及unsafe.Add(Go 1.21+)三类操作,并通过//go:linkname注释绑定具体函数签名。
生产环境HTTP头注入防御矩阵
| 风险点 | 检测方式 | 修复方案 | 实施位置 |
|---|---|---|---|
X-Forwarded-For伪造 |
net/http.Request.Header遍历 |
使用req.RemoteAddr结合可信代理列表校验 |
中间件层 |
Content-Type MIME混淆 |
mime.ParseMediaType()解析失败检测 |
拒绝charset=后含NUL字节或控制字符的值 |
请求预处理 |
Set-Cookie路径遍历 |
正则匹配Path=/.. |
强制标准化路径:filepath.Clean() + 前缀校验 |
Cookie生成器 |
静态分析规则嵌入Git Hooks
在.githooks/pre-commit中集成gosec与自定义规则:
# 检测硬编码密钥(含base64编码的敏感字符串)
gosec -exclude=G101 -out=gosec-report.json ./...
# 扫描SQL拼接模式(正则匹配"SELECT.*\+\s*\".*\".*FROM")
grep -r "sql\.Open\|database/sql" --include="*.go" . | grep -E '\+\s*".*".*FROM|Query.*\+'
内存安全加固的运行时防护
针对CNVD通报中暴露的sync.Pool滥用问题,在init()函数中注入监控:
var pool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 0, 4096)
runtime.SetFinalizer(&buf, func(b *[]byte) {
if cap(*b) > 1024*1024 {
log.Printf("WARN: oversized buffer in Pool, cap=%d", cap(*b))
}
})
return &buf
},
}
安全配置中心的动态策略下发
采用etcd作为配置源,通过go.etcd.io/etcd/client/v3监听/security/ratelimit/*路径变更,实时更新gin.HandlerFunc中的限流阈值,避免重启服务即可生效新策略。实测在K8s集群中,策略变更平均延迟
供应链组件可信验证流程
所有第三方模块均通过go mod verify校验后,再执行SHA256比对:
flowchart LR
A[go.sum checksum] --> B{是否匹配官方发布页?}
B -->|否| C[阻断CI流水线]
B -->|是| D[提取vendor/modules.txt]
D --> E[扫描CVE-2023-*关键词]
E --> F[生成SBOM并上传至内部仓库]
日志脱敏的结构化实施
在log/slog处理器中实现字段级过滤:
func SanitizeAttrs(attrs []slog.Attr) []slog.Attr {
for i := range attrs {
switch attrs[i].Key {
case "password", "token", "auth_key":
attrs[i].Value = slog.StringValue("[REDACTED]")
case "user_ip":
ip := net.ParseIP(attrs[i].Value.String())
if ip != nil && !ip.IsPrivate() {
attrs[i].Value = slog.StringValue(ip.To4().Mask(net.CIDRMask(24,32)).String())
}
}
}
return attrs
} 