问题描述
url.QueryUnescape
url 解码 - go/src/net/url/url.go
在 request_parse_module.go 模块会对 model 字段进行解码操作;会根据 url decode 规则把 model 转换成新的数据
model := rdata.DspRequest.Device.Model
if model != "" {
model, _ = utils.UrlDecode(model)
m.rdata.DspRequest.Device.Model = model
}
utf8.ValidString
字符串校验 - go/src/unicode/utf8/utf8.go
在通过 grpc 调用 filter server 前,会对 pb 数据进行转码操作;转码时,会先校验数据的合法性
if strs.EnforceUTF8(fd) && !utf8.ValidString(v.String()) {
return b, errors.InvalidUTF8(string(fd.FullName()))
}
这里校验时,model 字段是经过 步骤 1 转换后的数据,可能会导致校验失败
原因解析
url.QueryUnescape
- 在 url 生成时,可能会包含一些明文不可见或特殊字符;所以往往会对 url 进行 encode 编码操作,从而能够在 URL 中传递这些字符。比如字符 % 编码后变成 %25
- 所以在拿到 url 数据后,一般也需要对应的 decode 解码操作。解码时,会认为%或者+开头的字符是从其它特殊字符转换过来的,需要对其反转义;比如把上面的%25 三个字符反转成一个字符%
func unescape(s string, mode encoding) (string, error) {
...
var t strings.Builder
t.Grow(len(s) - 2*n)
for i := 0; i < len(s); i++ {
switch s[i] {
case '%':
t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2]))
i += 2
case '+':
if mode == encodeQueryComponent {
t.WriteByte(' ')
} else {
t.WriteByte('+')
}
default:
t.WriteByte(s[i])
}
}
return t.String(), nil
}
utf8.ValidString
UTF-8 是解决如何为 Unicode 编码而设计的一种编码规则。可以说 UTF-8 是 Unicode 的实现方式之一。其特点是一种变长编码,使用 1 到 4 个字节表示一个字符,根据不同的符号而变化长度。UTF-8 的编码规则有二:
- 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于 ASCII 码字符,UTF-8 编码和 ASCII 码是相同的。
- 对于 n 字节的符号(n > 1,2 到 4),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
以下是编码规则:
Unicode | UTF-8 |
---|---|
0000 0000-0000 007F | 0xxxxxxx |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
因为 utf8 编码中,一个字符可能对应 2-4 个字节;所以字符串的字节数并不一定等于其代表的 utf8 字符个数。如果字节数值小于 128,认为一个 utf8 字符等于一个字节;大于 128 时,认为一个 utf8 字符对应多个字节,然后根据首字节值计算 utf8 码的长度,取出剩余的字节,再验证是否符合上面的规则
//alidString reports whether s consists entirely of valid UTF-8-encoded runes.
func ValidString(s string) bool {
//n表示字符串s的字节数
n := len(s)
//i表示s所代表的utf8编码的字符个数
for i := 0; i < n; {
si := s[i]
//小于128,等于ascii码
if si < RuneSelf {
i++
continue
}
//x表示上面编码规则中 n > 1字节的的首字节
x := first[si]
if x == xx {
return false // Illegal starter byte.
}
//依照上面的规则,根据首字节计算utf8字符所对应的字节数
size := int(x & 7)
if i+size > n {
return false // Short or invalid.
}
accept := acceptRanges[x>>4]
if c := s[i+1]; c < accept.lo || accept.hi < c {
return false
} else if size == 2 {
} else if c := s[i+2]; c < locb || hicb < c {
return false
} else if size == 3 {
} else if c := s[i+3]; c < locb || hicb < c {
return false
}
i += size
}
return true
}
场景重现
demo 代码
package main
import (
"fmt"
"net/url"
)
const (
RuneError = '\uFFFD' // the "error" Rune or "Unicode replacement character"
RuneSelf = 0x80 // characters below RuneSelf are represented as themselves in a single byte.
MaxRune = '\U0010FFFF' // Maximum valid Unicode code point.
UTFMax = 4 // maximum number of bytes of a UTF-8 encoded Unicode character.
as = 0xF0 // ASCII: size 1
xx = 0xF1 // invalid: size 1
s1 = 0x02 // accept 0, size 2
s2 = 0x13 // accept 1, size 3
s3 = 0x03 // accept 0, size 3
s4 = 0x23 // accept 2, size 3
s5 = 0x34 // accept 3, size 4
s6 = 0x04 // accept 0, size 4
s7 = 0x44 // accept 4, size 4
locb = 0b10000000
hicb = 0b10111111
)
var first = [256]uint8{
// 1 2 3 4 5 6 7 8 9 A B C D E F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x00-0x0F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x10-0x1F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x20-0x2F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x30-0x3F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x40-0x4F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x50-0x5F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x60-0x6F
as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, as, // 0x70-0x7F
// 1 2 3 4 5 6 7 8 9 A B C D E F
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x80-0x8F
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0x90-0x9F
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xA0-0xAF
xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xB0-0xBF
xx, xx, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xC0-0xCF
s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, s1, // 0xD0-0xDF
s2, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s3, s4, s3, s3, // 0xE0-0xEF
s5, s6, s6, s6, s7, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, xx, // 0xF0-0xFF
}
type acceptRange struct {
lo uint8 // lowest value for second byte.
hi uint8 // highest value for second byte.
}
// acceptRanges has size 16 to avoid bounds checks in the code that uses it.
var acceptRanges = [16]acceptRange{
0: {locb, hicb},
1: {0xA0, hicb},
2: {locb, 0x9F},
3: {0x90, hicb},
4: {locb, 0x8F},
}
func main() {
data := "%dd"
newData, _ := url.QueryUnescape(data)
fmt.Println("data len:", len(data), " byte val:", []byte(data), " str val:", data)
fmt.Println("newData len:", len(newData), " byte val:", []byte(newData), " str val:", newData)
fmt.Println(ValidString(newData))
}
func ValidString(s string) bool {
n := len(s)
for i := 0; i < n; {
si := s[i]
if si < RuneSelf {
i++
continue
}
x := first[si]
if x == xx {
return false // Illegal starter byte.
}
size := int(x & 7)
if i+size > n {
return false // Short or invalid.
}
accept := acceptRanges[x>>4]
if c := s[i+1]; c < accept.lo || accept.hi < c {
return false
} else if size == 2 {
} else if c := s[i+2]; c < locb || hicb < c {
return false
} else if size == 3 {
} else if c := s[i+3]; c < locb || hicb < c {
return false
}
i += size
}
return true
}
代码解析
- 模拟 model 字段初始值为 %dd,其对应的[]byte 值为三个字节长度: [37 100 100]
- 进行 url.QueryUnescape 操作,因为%字符的存在,所以会认为%dd 是某个字符转义后的数据;解码后,数值变成 一个字节长度 [221]
- 针对解码后的数据 [221] 进行 utf8.ValidString 合法校验;因为首字节 221 > 128,所以认为其不是个 单字节 ascii 码;走到 83 行代码,认为其对应的 utf8 字符长度 size=2 为 2 个字节,但实际其字节数才为 1,所以 84 行判断失败,直接 return false
- 认为其字节缺失,不是个完整的 utf8 编码数据,报出 error