登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go html/template 生成 javascript URL 为什么被过滤

来源:17golang原创

时间:2026-10-20 20:43:56 450浏览 收藏

如果 Go 的 html/template 把你传入的 javascript:... 输出成 #ZgotmplZ,通常不是模板写错了,而是它在 href 这类 URL 上下文中主动拦截了危险协议。这样做是为了避免不可信数据变成可执行脚本。

官方地址:https://pkg.go.dev/html/template

这类问题的处理顺序很简单:先确认需求是不是“跳转到一个地址”,如果是,就改成 https 或站内路径;如果确实需要特殊协议,再由服务端对固定来源做 allowlist,最后才考虑 template.URL。不要把用户输入直接强转成可信类型。

为什么 javascript: 会变成 #ZgotmplZ

html/template 和 text/template 的关键区别是,它会结合 HTML、CSS、JavaScript、URI 等上下文进行转义。href="{{.URL}}" 中的值不是普通文本,而是一个可能影响浏览器行为的 URL。

对于普通字符串,模板包会检查协议。http:、https: 和 mailto: 属于文档明确列出的允许协议;其它协议会按不安全处理。命中 URL 过滤器后,输出中的值会被替换成 #ZgotmplZ,这正是它的运行时占位标记。

html/template 在 URL 上下文中从普通字符串经过协议过滤得到安全占位结果的静态结构图
图1:URL 上下文、协议过滤器和 #ZgotmplZ 的关系示意图;这是静态说明图,不是运行截图或执行证据。

普通字符串和 template.URL 不是一回事

默认情况下,模板把数据当成不可信的普通文本。下面的例子用同一个模板展示普通字符串和可信 URL 类型的职责差异:前者会被过滤,后者表示调用方已经完成了额外的安全判断。

package main

import (
	"os"
	"html/template"
)

func main() {
	// URL 上下文会对普通字符串做协议过滤。
	t := template.Must(template.New("link").Parse(`打开`))
	_ = t.Execute(os.Stdout, "javascript:alert('blocked')")

	// template.URL 只适合来自可信、固定来源的已审查值。
	trusted := template.URL("javascript:trustedAction()")
	_ = t.Execute(os.Stdout, trusted)
}

这里的第二种写法不是“关闭安全检查”的通用修复方案。官方文档明确提示,template.URL 的内容会按原样进入模板,因此只要来源可被用户控制,类型转换本身就可能制造注入风险。多数业务不应该让动态脚本通过 href 传递。

更稳妥的改法:让链接表达导航

如果点击动作本质上是打开页面、提交确认或进入下一步,优先把它建模成一个普通 HTTP 路由。脚本需要的业务参数放进查询参数或服务端会话中,模板只负责输出可导航的 URL。

package main

import (
	"html/template"
	"net/url"
	"os"
)

type PageData struct {
	ActionURL string
}

func main() {
	// 用 url.Values 编码参数,避免手工拼接破坏 URL 结构。
	q := url.Values{}
	q.Set("next", "/orders")
	data := PageData{ActionURL: "/confirm?" + q.Encode()}

	// 普通站内路径由 html/template 按 URL 上下文安全输出。
	t := template.Must(template.New("page").Parse(`继续`))
	_ = t.Execute(os.Stdout, data)
}

如果前端需要根据状态决定行为,可以输出受限的 data-action 值,让已经加载的脚本通过事件监听器查表处理。这样“可执行代码”和“服务端数据”仍然是两条边界,模板不会把任意字符串当作 JavaScript。

从外部动作数据经过协议 allowlist 到可信 URL 和 href 输出的静态架构图
图2:把业务动作拆成 allowlist、可信 URL 和普通 href 的结构示意图;它用于解释边界,不代表某次程序运行结果。

必须使用特殊协议时怎样收紧边界

少数内嵌应用可能确实有固定的特殊协议需求。这时要把“允许哪些完整字符串”写成代码中的固定集合,而不是接收一个任意 URL 后调用 template.URL(value)。

package main

import (
	"errors"
	"html/template"
	"strings"
)

var errUnsafeURL = errors.New("unsafe action URL")

func trustedActionURL(name string) (template.URL, error) {
	// 只匹配固定动作,不把 name 拼进可执行脚本。
	switch strings.TrimSpace(name) {
	case "open-help":
		return template.URL("javascript:openHelp()"), nil
	case "close-panel":
		return template.URL("javascript:closePanel()"), nil
	default:
		return "", errUnsafeURL
	}
}

即使这样,也要确认固定脚本不包含外部输入,并把这段转换集中在少数可审查函数中。更推荐使用普通链接和事件映射;allowlist 只能缩小风险,不能把 javascript: 变成适合任意数据的协议。

几个容易混淆的边界

现象实际含义处理方式
#ZgotmplZ不安全值到达 URL 或 CSS 上下文检查协议和数据来源,不要只改输出字符串
template.URL调用方声明内容已经可信只接收固定或严格 allowlist 的值
JavaScript 上下文,不是 href URL 上下文传递结构化数据,让模板负责 JS 安全编码
template.HTML声明整段 HTML 已可信不要拿它解决 URL 协议问题

排查结论

遇到 “javascript URL 被过滤” 时,可以按下面的顺序处理:

  1. 确认占位值是否来自 href、src 等 URL 上下文。
  2. 检查输入是不是普通字符串,以及协议是否为 http、https 或站内相对路径。
  3. 若需求只是导航,改用普通路由和参数编码。
  4. 若确有特殊协议,先做固定值 allowlist,再集中转换为 template.URL。
  5. 不要用 template.HTML、字符串替换或关闭转义来掩盖问题。

相关问题

为什么不是 Execute 返回错误?

URL 过滤属于模板输出安全处理的一部分,危险值通常会以 #ZgotmplZ 进入结果,而不是作为普通业务错误返回。

把 javascript: 改成 URL 编码可以吗?

不应把编码当成安全修复。应该先决定动作是否真的需要脚本,再选择普通路由或固定动作映射。

template.URL 能不能接收数据库字段?

只有当数据库字段来自受控配置、经过明确协议和内容 allowlist,并且不会混入用户可控脚本时才应考虑;普通业务字段不应直接转换。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>