仓颉三方库开发实战:sanitize_html 实现详解
仓颉三方库开发实战:sanitize_html 实现详解
项目背景
在现代Web开发中,HTML内容清理和消毒是防止XSS(跨站脚本)攻击的关键技术。sanitize_html项目使用仓颉编程语言实现了一个功能强大的HTML内容清理和消毒库,不仅提供了完善的HTML过滤功能,也探索了仓颉语言在Web安全领域的应用潜力。
本文将详细介绍sanitize_html的设计理念、核心功能实现、技术挑战与解决方案,为使用仓颉语言进行Web安全开发的开发者提供参考。
技术栈
- 开发语言:仓颉编程语言 (cjc >= 1.0.3)
- 核心库:
- std.collection: 数据结构(ArrayList, HashMap)
- std.option: Option类型错误处理
- 字符串处理:Rune类型处理、字符串操作
核心功能实现
1. 库架构设计
sanitize_html采用分层模块化设计,将功能分解为多个独立的模块:
sanitize_html
├── 数据结构定义模块 # Frame、Tag、SanitizeOptions等核心数据结构
├── 字符串工具模块 # 字符串处理、转义、匹配等工具函数
├── URL解析和验证模块 # URL解析、协议验证、域名检查等
├── HTML解析器模块 # HTML标签解析、属性提取、文本处理等
├── 标签过滤模块 # 标签白名单、属性过滤、类名过滤等
├── 标签转换模块 # 标签转换器、simpleTransform函数等
├── 文本过滤模块 # 文本过滤器、文本处理模式等
├── 排除过滤模块 # 排除过滤器、排除规则匹配等
└── 公共API模块 # sanitize、sanitizeHtml等公共接口
设计亮点:
- 模块化设计,提高代码可维护性和可测试性
- 接口驱动设计,支持灵活的扩展机制
- 类型安全,充分利用仓颉语言的类型系统
2. 核心数据结构设计
Frame类:标签层次结构跟踪
Frame类用于跟踪HTML标签的层次结构,确保标签正确闭合和嵌套:
public class Frame {
public var tag: String // 原始标签名(用于匹配结束标签)
public var transformedTag: String // 转换后的标签名(用于输出结束标签)
public var attribs: HashMap<String, String>
public var tagPosition: Int64
public var text: String
public var openingTagLength: Int64
public var mediaChildren: ArrayList<String>
public var innerText: Option<String>
public init(tag: String, attribs: HashMap<String, String>) {
this.tag = tag
this.transformedTag = tag
this.attribs = attribs
this.tagPosition = 0
this.text = ""
this.openingTagLength = 0
this.mediaChildren = ArrayList<String>()
this.innerText = None
}
}
设计考虑:
- 使用栈(ArrayList)跟踪标签嵌套关系
- 区分原始标签名和转换后的标签名,确保结束标签匹配正确
- 使用Option类型处理可选的innerText,避免空指针异常
Tag类:标签转换数据结构
Tag类用于标签转换器返回完整信息:
public class Tag {
public var tagName: String
public var attribs: HashMap<String, String>
public var text: Option<String> // 可选的文本内容
public init(tagName: String, attribs: HashMap<String, String>, text: Option<String>) {
this.tagName = tagName
this.attribs = attribs
this.text = text
}
// 便捷构造函数
public static func of(tagName: String, attribs: HashMap<String, String>): Tag {
return Tag(tagName, attribs, None)
}
public static func of(tagName: String, attribs: HashMap<String, String>, text: String): Tag {
return Tag(tagName, attribs, Some(text))
}
}
设计考虑:
- 支持标签名转换、属性修改、文本内容设置三种转换方式
- 使用便捷构造函数简化Tag对象创建
- Option类型处理可选的文本内容
SanitizeOptions类:配置选项管理
SanitizeOptions类包含所有可配置的选项:
public class SanitizeOptions {
public var allowedTags: Option<ArrayList<String>>
public var allowedAttributes: Option<HashMap<String, ArrayList<String>>>
public var allowedSchemes: ArrayList<String>
// ... 更多配置选项
public init() {
// 初始化所有字段为默认值
}
public static func defaults(): SanitizeOptions {
let opts = SanitizeOptions()
// 设置安全的默认配置
opts.allowedTags = Some(SanitizeOptions.getDefaultTags())
opts.allowedAttributes = Some(SanitizeOptions.getDefaultAttributes())
// ... 设置其他默认值
return opts
}
}
设计考虑:
- 使用Option类型表示可选配置,None表示使用默认行为
- 提供defaults()静态方法,返回安全的默认配置
- 支持用户配置与默认配置的智能合并
3. HTML解析器实现
HTML解析器是sanitize_html的核心模块,负责解析HTML标签、属性、文本内容等。
解析流程
private func parseAndSanitize(html: String, options: SanitizeOptions): String {
// 1. 预处理CDATA(如果启用)
var processedHtml = html
if (options.recognizeCDATA) {
processedHtml = preprocessCDATA(html)
}
// 2. 初始化状态
var result = ""
let stack = ArrayList<Frame>()
var depth: Int64 = 0
var skipText = false
var skipTextDepth: Int64 = 0
let htmlRunes = processedHtml.toRuneArray()
var i: Int64 = 0
// 3. 遍历HTML字符
while (i < htmlRunes.size) {
if (htmlRunes[i] == r'<') {
// 处理标签
let tagEnd = findTagEnd(htmlRunes, i + 1)
if (tagEnd.isSome()) {
let end = tagEnd.unwrap()
let tagContentStr = runesToStringSlice(htmlRunes, i+1, end)
if (stringStartsWith(tagContentStr, "/")) {
// 处理结束标签
handleClosingTag(...)
} else {
// 处理开始标签
handleOpeningTag(...)
}
}
} else {
// 处理文本内容
if (!skipText) {
handleText(...)
}
}
}
// 4. 关闭所有未闭合的标签
closeUnclosedTags(stack, result)
// 5. 后处理CDATA(如果启用)
if (options.recognizeCDATA) {
result = postprocessCDATA(result)
}
return result
}
实现要点:
- 使用Rune数组进行字符处理,正确支持Unicode字符
- 使用栈跟踪标签嵌套关系,确保标签正确闭合
- 支持CDATA段的预处理和后处理
- 支持skipText机制,跳过某些标签的内容(如script、style)
标签解析
private func parseTag(tagContent: String): (String, HashMap<String, String>) {
let tagName = extractTagName(tagContent)
let attrs = HashMap<String, String>()
let runes = tagContent.toRuneArray()
var i = stringSize(tagName)
// 解析属性
while (i < runes.size) {
let oldI = i
i = parseAttribute(runes, i, attrs)
// 防止无限循环
if (i <= oldI) {
i = oldI + 1
}
}
return (tagName, attrs)
}
实现要点:
- 支持带引号和不带引号的属性值
- 处理属性的各种格式(单引号、双引号、无引号)
- 防止解析死循环,确保解析进度
4. 标签过滤机制
标签过滤是sanitize_html的核心安全机制之一。
标签白名单检查
private func tagAllowed(name: String, options: SanitizeOptions): Bool {
match (options.allowedTags) {
case None => return true // 未指定则允许所有
case Some(allowed) => return isTagInList(name, allowed)
}
}
禁用标签处理模式
sanitize_html支持三种禁用标签处理模式:
- discard(默认):丢弃禁用标签,保留其内容
- escape:转义禁用标签,子标签如果不被禁用则不转义
- recursiveEscape:转义禁用标签及其所有子标签,不管子标签是否被允许
if (!tagAllowed(finalTagName, options)) {
if (options.disallowedTagsMode == "discard" || options.disallowedTagsMode == "completelyDiscard") {
if (containsInList(options.nonTextTags, tagName)) {
newSkipText = true
newSkipTextDepth = newDepth
}
} else if (options.disallowedTagsMode == "escape" || options.disallowedTagsMode == "recursiveEscape") {
let tagStr = runesToStringSlice(htmlRunes, i, end+1)
result = escapeHtml(tagStr, false)
}
}
5. 属性过滤机制
属性过滤是sanitize_html的另一个核心安全机制。
属性白名单检查
private func attributeAllowed(tagName: String, attrName: String, options: SanitizeOptions): Bool {
match (options.allowedAttributes) {
case None => return true
case Some(allowedAttrs) => return checkAttributesInMap(allowedAttrs, tagName, attrName)
}
}
URL属性验证
对于href、src等URL属性,需要进行协议验证:
private func naughtyHref(tagName: String, href: String, options: SanitizeOptions): Bool {
// 移除控制字符
var cleaned = cleanUrl(href)
// 移除注释
cleaned = removeComments(cleaned)
// 提取协议
let scheme = extractScheme(cleaned)
match (scheme) {
case Some(sch) => return handleSchemeExists(tagName, sch, options)
case None => return handleSchemeNone(cleaned, options)
}
}
实现要点:
- 移除URL中的控制字符和注释,防止XSS攻击
- 验证URL协议是否在允许列表中
- 支持协议相对URL(//example.com)的处理
- 支持按标签配置不同的协议规则
类名过滤
对于class属性,支持类名白名单过滤:
private func filterClasses(classes: String, allowed: Option<HashMap<String, ArrayList<String>>>, tagName: String): String {
match (allowed) {
case None => classes // 没有限制,返回原值
case Some(allowedMap) => filterClassesWithMap(classes, allowedMap, tagName)
}
}
实现要点:
- 支持按标签配置允许的类名列表
- 支持通配符匹配(如 “class-*” 匹配所有以 “class-” 开头的类名)
- 支持正则表达式匹配(以 “regex:” 开头)
6. 标签转换功能
sanitize_html支持两种标签转换方式:
字符串映射
简单的标签名映射:
options.transformTags["ol"] = "ul" // 将ol标签转换为ul
函数转换器
通过TagTransformer接口实现复杂的转换逻辑:
public interface TagTransformer {
func transform(tagName: String, attributes: HashMap<String, String>): Option<Tag>
}
// 使用simpleTransform创建转换器
let transformer = simpleTransform("div", None, None)
options.transformTagFunctions["p"] = transformer
实现要点:
- 支持精确匹配和通配符匹配(如 “h*” 匹配所有h开头的标签)
- 支持属性合并和替换两种模式
- 支持在转换时设置标签的innerText
7. 文本过滤功能
sanitize_html支持两种文本过滤方式:
预设模式
options.textFilterMode = "trim|upper" // 去除首尾空白并转大写
支持的预设模式:
- trim:去除首尾空白
- upper:转大写
- lower:转小写
函数形式
通过TextFilter接口实现自定义文本过滤逻辑:
public interface TextFilter {
func filter(text: String, tagName: String): String
}
class UpperCaseFilter <: TextFilter {
public func filter(text: String, _: String): String {
// 转换为大写
return toUpperCase(text)
}
}
8. 排除过滤功能
sanitize_html支持两种排除过滤方式:
配置化规则
通过ExclusionRule实现配置化的排除规则:
public class ExclusionRule {
public var tag: String // 要匹配的标签名
public var attributeConditions: HashMap<String, String> // 属性条件
public var textCondition: Option<String> // 文本内容条件
public var mode: String // "excludeTag" 或 "excludeAll"
}
let rule = ExclusionRule("a", HashMap<String, String>(), None, "excludeTag")
options.exclusiveFilterRules.add(rule)
函数形式
通过ExclusiveFilter接口实现自定义排除逻辑:
public interface ExclusiveFilter {
func shouldExclude(frame: Frame): ExcludeResult
}
class EmptyLinkFilter <: ExclusiveFilter {
public func shouldExclude(frame: Frame): ExcludeResult {
if (frame.tag == "a" && frame.text == "") {
return ExcludeResult.excludeTag()
}
return ExcludeResult.none()
}
}
实现要点:
- 支持excludeTag(只排除标签)和excludeAll(排除标签和内容)两种模式
- 支持按标签名、属性条件、文本内容进行匹配
- 支持正则表达式匹配(属性值和文本内容)
技术挑战与解决方案
1. Unicode字符处理
挑战:HTML可能包含各种Unicode字符,需要正确处理。
解决方案:使用Rune类型进行字符处理:
private func toLowerCase(s: String): String {
var result = ""
for (r in s.runes()) {
if (r >= r'A' && r <= r'Z') {
result = result + String(Rune(UInt32(r) + 32))
} else {
result = result + String(r)
}
}
return result
}
优势:
- 正确处理多字节Unicode字符
- 避免字符串编码问题
- 提高字符串处理性能
2. 标签层次结构管理
挑战:HTML标签可能嵌套,需要正确跟踪标签的层次结构,确保标签正确闭合。
解决方案:使用栈(ArrayList)跟踪标签嵌套关系:
let stack = ArrayList<Frame>()
// 开始标签时入栈
let frame = Frame(tagName, filteredAttrs)
stack.add(frame)
// 结束标签时出栈
let popResult = popMatchingTag(stack, tagName)
实现细节:
- 使用Frame类存储标签信息,包括原始标签名和转换后的标签名
- popMatchingTag函数从栈顶向下查找匹配的标签,找到后删除从该位置到栈顶的所有元素(这是HTML标签匹配的标准行为)
- 处理未闭合标签,在解析结束时自动关闭
3. URL协议验证
挑战:需要验证URL协议的安全性,防止javascript:、data:等危险协议。
解决方案:实现URL解析和协议验证机制:
private func extractScheme(url: String): Option<String> {
// 首先查找 :// 模式(标准协议格式)
let colonSlashIndex = findSubstring(url, "://")
match (colonSlashIndex) {
case Some(idx) => return validateAndExtractScheme(url, idx)
case None => ()
}
// 如果没有找到 ://,查找单独的 :(用于javascript:等协议)
let colonIndex = findSubstring(url, ":")
match (colonIndex) {
case Some(idx) => return checkColonScheme(url, idx)
case None => return None
}
}
安全措施:
- 移除URL中的控制字符和注释
- 验证协议格式(字母开头,包含字母、数字、点、减号、加号)
- 支持协议白名单机制
- 支持按标签配置不同的协议规则
4. 标签转换的复杂性
挑战:标签转换需要支持多种转换方式,包括字符串映射、函数转换器、通配符匹配等。
解决方案:实现多层次的转换机制:
private func applyTransformTag(tagName: String, attributes: HashMap<String, String>,
transformTags: HashMap<String, String>,
transformTagFunctions: HashMap<String, TagTransformer>): Option<Tag> {
// 1. 首先尝试函数转换器(精确匹配)
// 2. 尝试函数转换器(通配符匹配)
// 3. 尝试字符串映射(精确匹配)
// 4. 尝试字符串映射(通配符匹配)
// 5. 没有匹配的规则,返回None
}
实现要点:
- 优先级:函数转换器 > 字符串映射(精确匹配 > 通配符匹配)
- 支持通配符模式匹配(如 “h*” 匹配所有h开头的标签)
- 支持属性合并和替换两种模式
- 支持在转换时设置标签的innerText
5. 性能优化
挑战:大型HTML文档的解析性能可能受到影响。
解决方案:
- 使用HashMap存储配置:提高查找效率
- 使用Rune数组进行字符处理:避免多次字符串复制
- 使用ArrayList作为标签栈:高效管理标签层次结构
- 延迟字符串构建:只在需要时构建HTML字符串
// 使用HashMap存储配置
let allowedAttrs = HashMap<String, ArrayList<String>>()
// 使用Rune数组进行字符处理
let htmlRunes = processedHtml.toRuneArray()
// 使用ArrayList作为标签栈
let stack = ArrayList<Frame>()
使用示例
示例1:基本使用
import sanitize_html.*
main() {
let dirty = "<script>alert('XSS')</script><p>Hello World</p>"
let clean = sanitize(dirty)
println(clean) // 输出: <p>Hello World</p>
}
示例2:自定义配置
import sanitize_html.*
import std.collection.ArrayList
import std.collection.HashMap
main() {
let options = SanitizeOptions()
let allowedTags = ArrayList<String>()
allowedTags.add("p")
allowedTags.add("b")
allowedTags.add("i")
options.allowedTags = Some(allowedTags)
let allowedAttrs = HashMap<String, ArrayList<String>>()
let pAttrs = ArrayList<String>()
pAttrs.add("class")
allowedAttrs["p"] = pAttrs
options.allowedAttributes = Some(allowedAttrs)
let dirty = "<p class=\"text\">Hello <b>World</b></p><script>alert('XSS')</script>"
let clean = sanitizeHtml(dirty, Some(options))
println(clean) // 输出: <p class="text">Hello <b>World</b></p>
}
示例3:标签转换
import sanitize_html.*
import std.collection.HashMap
main() {
let options = SanitizeOptions()
// 字符串映射
options.transformTags["ol"] = "ul"
// 函数转换器
let transformer = simpleTransform("div", None, None)
options.transformTagFunctions["p"] = transformer
let dirty = "<ol><li>Item</li></ol><p>Hello</p>"
let clean = sanitizeHtml(dirty, Some(options))
println(clean) // 输出: <ul><li>Item</li></ul><div>Hello</div>
}
示例4:文本过滤
import sanitize_html.*
class UpperCaseFilter <: TextFilter {
public func filter(text: String, _: String): String {
var result = ""
for (r in text.runes()) {
if (r >= r'a' && r <= r'z') {
result = result + String(Rune(UInt32(r) - 32))
} else {
result = result + String(r)
}
}
return result
}
}
main() {
let options = SanitizeOptions()
options.textFilter = Some(UpperCaseFilter())
let dirty = "<p>Hello World</p>"
let clean = sanitizeHtml(dirty, Some(options))
println(clean) // 输出: <p>HELLO WORLD</p>
}
示例5:排除过滤
import sanitize_html.*
import std.collection.ArrayList
import std.collection.HashMap
class EmptyLinkFilter <: ExclusiveFilter {
public func shouldExclude(frame: Frame): ExcludeResult {
if (frame.tag == "a" && frame.text == "") {
return ExcludeResult.excludeTag()
}
return ExcludeResult.none()
}
}
main() {
let options = SanitizeOptions()
options.exclusiveFilter = Some(EmptyLinkFilter())
let dirty = "<a href=\"#\"></a><a href=\"https://example.com\">Link</a>"
let clean = sanitizeHtml(dirty, Some(options))
println(clean) // 输出: <a href="https://example.com">Link</a>
}
最佳实践
1. 使用默认配置
对于大多数场景,使用默认配置即可获得良好的安全保护:
let clean = sanitize(dirty)
2. 最小权限原则
只允许必要的标签和属性:
let options = SanitizeOptions()
let allowedTags = ArrayList<String>()
allowedTags.add("p")
allowedTags.add("b")
options.allowedTags = Some(allowedTags)
3. URL协议白名单
严格限制允许的URL协议:
options.allowedSchemes.add("http")
options.allowedSchemes.add("https")
// 不包含javascript:、data:等危险协议
4. 域名白名单
对于script和iframe标签,使用域名白名单:
options.allowedScriptDomains.add("cdn.example.com")
options.allowedIframeDomains.add("youtube.com")
5. 测试覆盖
为自定义配置编写测试用例:
@Test
public class SanitizeHtmlTests {
@TestCase
func testBasicSanitize(): Unit {
let dirty = "<script>alert('XSS')</script><p>Hello</p>"
let clean = sanitize(dirty)
@Assert(clean == "<p>Hello</p>")
}
}
总结
sanitize_html是一个功能强大的HTML内容清理和消毒库,使用仓颉编程语言实现。通过模块化设计、接口驱动架构、类型安全机制,实现了完善的HTML过滤功能,有效防止XSS攻击。
核心优势:
- 类型安全:充分利用仓颉语言的类型系统,使用Option类型进行安全的错误处理
- 模块化设计:将功能分解为多个独立模块,提高代码可维护性和可测试性
- 接口驱动:提供丰富的接口(TagTransformer、TextFilter等),支持灵活的扩展机制
- 安全可靠:内置多层安全机制,有效防止XSS攻击
- 易于使用:提供简洁的API和安全的默认配置,开箱即用
适用场景:
- Web应用的用户输入过滤
- 内容管理系统(CMS)的HTML清理
- 富文本编辑器的内容处理
- 邮件系统的HTML内容过滤
- 任何需要HTML内容清理和消毒的场景
相关学习资源
仓颉标准库:https://gitcode.com/Cangjie/cangjie_runtime/tree/main/stdlib
仓颉扩展库:https://gitcode.com/Cangjie/cangjie_stdx
仓颉命令行工具:https://gitcode.com/Cangjie/cangjie_tools
仓颉语言测试用例:https://gitcode.com/Cangjie/cangjie_test
仓颉语言示例代码:https://gitcode.com/Cangjie/Cangjie-Examples
仓颉鸿蒙示例应用:https://gitcode.com/Cangjie/HarmonyOS-Examples
精品三方库:https://gitcode.com/org/Cangjie-TPC/repos
SIG 孵化库:https://gitcode.com/org/Cangjie-SIG/repos
sanitize_html展示了仓颉语言在Web安全领域的应用潜力,为使用仓颉语言进行Web开发的开发者提供了有价值的参考。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)