fix anwser token and database issus

This commit is contained in:
tamakyi
2026-09-15 11:06:09 +08:00
parent e683e798aa
commit adcc42cc8c
33 changed files with 1011 additions and 802 deletions
+12 -25
View File
@@ -15,10 +15,9 @@ import (
"golang.org/x/net/html"
)
// blockEndTags 与 reBlockEndBrList:删除块级元素闭合标签紧前的 <br>。
// HardLineBreak 会把块级元素末尾的换行也渲染成 <br>,产生多余空行;
// 这里仅清除末尾的 <br>(可含中间空白),行内真正的换行保留。
// 注意:RE2 不支持 lookahead,故对每个闭合标签单独编译。
// blockEndTags / reBlockEndBrList 删除块级元素闭合标签紧前的 <br>:HardLineBreak 会把
// 块级元素末尾的换行也渲染成 <br>,产生多余空行(行内换行保留)。
// RE2 不支持 lookahead,故对每个闭合标签单独编译。
var blockEndTags = []string{
"</li>", "</p>", "</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>",
"</blockquote>", "</pre>", "</div>",
@@ -27,7 +26,6 @@ var blockEndTags = []string{
var reBlockEndBrList = func() []*regexp.Regexp {
res := make([]*regexp.Regexp, 0, len(blockEndTags))
for _, tag := range blockEndTags {
// 捕获闭合标签本身,替换时用 $1 保留
res = append(res, regexp.MustCompile(`<br>[ \t\r\n]*(`+regexp.QuoteMeta(tag)+`)`))
}
return res
@@ -36,8 +34,7 @@ var reBlockEndBrList = func() []*regexp.Regexp {
// reBlankLines 压缩连续换行为单个换行(MarkdownToPlainText 提取时块级/br 会产生多个 \n)。
var reBlankLines = regexp.MustCompile(`\n{2,}`)
// PlainText 渲染 Markdown 为纯文本并压缩连续空行为单个换行,
// 供列表/摘要等紧凑展示场景使用(保留文字与换行,不显示 Markdown 符号与排版)。
// PlainText 渲染为纯文本并压缩连续空行,供列表/摘要等紧凑场景使用。
func PlainText(markdown string) string {
if strings.TrimSpace(markdown) == "" {
return ""
@@ -45,15 +42,9 @@ func PlainText(markdown string) string {
return reBlankLines.ReplaceAllString(MarkdownToPlainText(markdown), "\n")
}
// MarkdownToHTML 将 Markdown 渲染为安全的 HTML 片段(供模板直接输出)。
//
// 安全性:先用 blackfriday 渲染,再经白名单过滤器清洗:
// - 只保留常用排版标签与属性(a[href]/img[src,alt,title] 等)
// - 丢弃 script / style / iframe / object / embed / form 等危险标签
// - href/src 仅允许 http/https/mailto,其余一律去除
// - 剥离所有 on* 事件属性
//
// 这样既支持 Markdown 排版,又不会引入存储型 XSS。
// MarkdownToHTML 渲染为安全的 HTML 片段。先由 blackfriday 渲染,再经白名单过滤:
// 只保留常用排版标签与属性;丢弃 script / style / iframe / form 等危险标签;
// href/src 仅允许 http/https/mailto;剥离所有 on* 事件属性。避免存储型 XSS。
func MarkdownToHTML(markdown string) template.HTML {
if strings.TrimSpace(markdown) == "" {
return ""
@@ -117,7 +108,6 @@ func extractPlainText(raw []byte) string {
}
}
// 允许的标签集合。
var allowedTags = map[string]bool{
"p": true, "br": true, "hr": true, "div": true, "span": true,
"strong": true, "em": true, "b": true, "i": true, "u": true, "s": true,
@@ -141,13 +131,12 @@ func sanitizeHTML(raw []byte) string {
case html.ErrorToken:
return out.String()
case html.TextToken:
// Tokenizer.Text returns unescaped text. Escape it again before
// rebuilding HTML, otherwise input such as &lt;script&gt; can become
// executable markup in the template.HTML result.
// ⚠️ Tokenizer.Text 返回已反转义文本,重建 HTML 前必须重新转义,
// 否则 &lt;script&gt; 这类输入会在 template.HTML 结果中变成可执行标记。
out.WriteString(stdhtml.EscapeString(string(tokenizer.Text())))
case html.StartTagToken, html.SelfClosingTagToken:
// SelfClosingTagToken 覆盖 `<br />` 这类自闭合写法(blackfriday 的硬换行即此形式),
// 漏掉会导致 <br> 被丢弃、换行失效。
// SelfClosingTagToken 覆盖 `<br />` 自闭合写法(blackfriday 硬换行即此形式),
// 漏掉会丢弃 <br>、换行失效。
writeSanitizedTag(&out, tokenizer.Token(), false)
case html.EndTagToken:
writeSanitizedTag(&out, tokenizer.Token(), true)
@@ -182,7 +171,6 @@ func writeSanitizedTag(out *bytes.Buffer, tok html.Token, isEnd bool) {
key := strings.ToLower(attr.Key)
val := strings.TrimSpace(attr.Val)
// 过滤事件属性与危险属性
if strings.HasPrefix(key, "on") {
continue
}
@@ -247,8 +235,7 @@ func safeURL(u string) bool {
lower := strings.ToLower(strings.TrimSpace(u))
// 必须先于下面的 "/" 前缀判断:协议相对 URL(//evil.com)同样以 "/" 开头,
// 若不单独拦截会被误判为站内相对路径,从而绕过"仅 http/https/mailto"的白名单本意,
// 使 markdown 中的 [x](//evil.com) 渲染成指向外站的链接(钓鱼 / 外链埋点)。
// 不单独拦截会被误判为站内相对路径,[x](//evil.com) 即可指向外站(钓鱼 / 外链埋点)。
// 反斜杠变体(\\evil.com、/\evil.com)在部分浏览器中等价于 //,一并拒绝。
if strings.HasPrefix(lower, "//") || strings.HasPrefix(lower, `\\`) ||
strings.HasPrefix(lower, `/\`) {