仓颉三方库开发实战: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: Int64public var text: Stringpublic var openingTagLength: Int64public var mediaChildren: ArrayList<String>public var innerText: Option<String>public init(tag: String, attribs: HashMap<String, String>) {this.tag = tagthis.transformedTag = tagthis.attribs = attribsthis.tagPosition = 0this.text = ""this.openingTagLength = 0this.mediaChildren = ArrayList<String>()this.innerText = None}
}
设计考虑:
- 使用栈(ArrayList)跟踪标签嵌套关系
- 区分原始标签名和转换后的标签名,确保结束标签匹配正确
- 使用Option类型处理可选的innerText,避免空指针异常
Tag类:标签转换数据结构
Tag类用于标签转换器返回完整信息:
public class Tag {public var tagName: Stringpublic var attribs: HashMap<String, String>public var text: Option<String> // 可选的文本内容public init(tagName: String, attribs: HashMap<String, String>, text: Option<String>) {this.tagName = tagNamethis.attribs = attribsthis.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 = htmlif (options.recognizeCDATA) {processedHtml = preprocessCDATA(html)}// 2. 初始化状态var result = ""let stack = ArrayList<Frame>()var depth: Int64 = 0var skipText = falsevar skipTextDepth: Int64 = 0let 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 = ii = 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 = truenewSkipTextDepth = 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 truecase 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.HashMapmain() {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"] = pAttrsoptions.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.HashMapmain() {let options = SanitizeOptions()// 字符串映射options.transformTags["ol"] = "ul"// 函数转换器let transformer = simpleTransform("div", None, None)options.transformTagFunctions["p"] = transformerlet 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.HashMapclass 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 {@TestCasefunc 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开发的开发者提供了有价值的参考。
