当前位置:首页 > 文章列表 > Golang > Go问答 > html/template URL 上下文为何拒绝某些协议

html/template URL 上下文为何拒绝某些协议

来源:17golang原创 2026-11-13 11:24:11 0浏览 收藏

我第一次遇到这个现象,是把数据库里保存的跳转地址直接放进 href,页面却出现了 #ZgotmplZ。这不是 HTTP 请求失败,也不是浏览器把链接改坏了,而是 html/template 发现动态值处在 URL 上下文中,并拒绝了它的协议。默认情况下,相对 URL、http:、https: 和 mailto: 可以通过;其它协议会被安全地降级。

看到 #ZgotmplZ 时,先检查 URL 的原始字符串、模板所在的属性上下文和数据类型。不要先把字符串强行转换成 template.URL;正确顺序是先在业务层限制协议,再让模板完成上下文转义。

先看链接任务本身发生了什么

URL 之所以特殊,是因为同一个字符串放在普通文本里只是文本,放进 href 或 src 后却可能改变浏览器行为。html/template 的安全模型默认信任模板作者,不默认信任执行模板时传入的数据,因此会根据插值出现的位置选择转义和过滤策略。

下面的例子把四类地址放进同一个链接模板。示例只观察模板输出,不发起任何网络访问。

package main

import (
    "bytes"
    "fmt"
    "html/template"
)

func main() {
    // href 是 URL 上下文,模板会对传入值做协议判断。
    tmpl := template.Must(template.New("link").Parse(`打开`))
    values := []string{
        "/docs/start",                    // 相对 URL
        "https://example.com/docs",       // 默认允许的网络协议
        "mailto:team@example.com",        // 默认允许的邮件协议
        "javascript:alert(1)",            // 会被 URL 过滤器拒绝
    }

    for _, value := range values {
        var out bytes.Buffer
        // 每次单独执行,便于把输入值和最终 HTML 对照起来。
        if err := tmpl.Execute(&out, value); err != nil {
            panic(err)
        }
        fmt.Printf("%s => %s\n", value, out.String())
    }
}
/docs/start => 打开
https://example.com/docs => 打开
mailto:team@example.com => 打开
javascript:alert(1) => 打开

最后一行的值没有被当成一个普通的“链接文本”输出,而是被识别为可能触发副作用的协议。这个替换发生在模板执行阶段,和之后是否真的点击链接、服务器是否能访问目标地址是两回事。

协议过滤发生在 URL 上下文的哪一层

可以把动态值的路径理解为:业务数据进入模板插值,模板先判断当前位置是不是 URL 上下文,再判断字符串里的协议。如果字符串是相对路径,或者协议是 http、https、mailto,它会继续做 URL 规范化和 HTML 属性转义;如果是其它协议,则输出 #ZgotmplZ 这一安全兜底值。

html/template URL 上下文协议过滤边界结构图
图1:URL 上下文把相对 URL、http、https、mailto 与被拒绝协议分到不同出口的静态结构图,不是截图或运行证据。

Go 官方源码中的判断是保守的:只要字符串里出现了不在允许集合中的协议,就按不安全处理。协议名大小写也不应成为业务层的绕过点,因为源码会用不区分大小写的方式比较允许协议。

输入形态默认结果排查重点
/account通常保留确认它确实是相对 URL,而不是拼接出的未知协议
https://example.com通常保留并转义属性字符仍要在业务层限制域名和跳转范围
mailto:user@example.com通常保留按业务需求限制收件人和参数
javascript:... 或其它协议变成 #ZgotmplZ不要用类型转换掩盖来源问题

把协议选择放在业务层

模板过滤解决的是“这个值处在 URL 上下文,是否应该原样进入 HTML”的问题,不等于业务 URL 校验。比如产品只允许站内路径,就不该因为 https 默认可通过而放开任意外站;如果产品确实支持外链,也应该先限制协议、主机或路径,再把普通字符串交给模板。

下面的辅助函数只把相对路径和指定的 https 主机放行。它返回普通字符串,而不是 template.URL,这样最终的 HTML 属性转义仍由 html/template 负责。

package links

import (
    "net/url"
    "strings"
)

func safeLink(raw string) (string, bool) {
    // 先解析,避免只靠字符串前缀判断协议和主机。
    u, err := url.Parse(raw)
    if err != nil || u.IsAbs() && u.Host == "" {
        return "", false
    }

    // 空协议只允许站内相对地址,禁止把 //host 当作站内路径。
    if u.Scheme == "" {
        if u.Host != "" || !strings.HasPrefix(u.Path, "/") {
            return "", false
        }
        return u.String(), true
    }

    // 外链协议和主机是产品策略,和模板的默认过滤是两层不同的边界。
    if !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Host, "docs.example.com") {
        return "", false
    }
    return u.String(), true
}
业务 URL allowlist 与 html/template 转义边界结构图
图2:业务层先限制协议,再把普通字符串交给模板;可信类型是明确的例外边界,不是通用绕过开关。

这里有两个容易混淆的点。第一,业务 allowlist 不能代替模板的上下文转义,最终仍要使用 html/template 而不是把字符串拼接进 HTML。第二,模板默认允许某个协议,也不代表业务一定应该允许它;安全默认值和产品规则要分别表达。

什么时候可以使用 template.URL

template.URL 是一个明确的“这段 URL 内容已经由可信代码确认”的类型。它会让 URL 过滤器把值视为可信 URL,因此可以处理默认协议集合之外、但确实被模板作者固定控制的内容。

package main

import (
    "html/template"
    "os"
)

type PageData struct {
    SupportLink template.URL
}

func main() {
    // 这个值是程序内固定常量,不来自请求参数、数据库字段或用户配置。
    data := PageData{SupportLink: template.URL("tel:+8613800000000")}
    tmpl := template.Must(template.New("support").Parse(`拨打支持电话`))

    // 模板仍负责把结果放进正确的属性上下文;可信类型只表示调用方已承担内容安全责任。
    if err := tmpl.Execute(os.Stdout, data); err != nil {
        panic(err)
    }
}

不要把下面这种写法当成修复:

// 错误示例:不要把不可信输入直接包装成 template.URL。
data.SupportLink = template.URL(r.URL.Query().Get("link"))

这等于把安全判断从模板手里拿走,却没有补上业务层的判断。只要数据可能来自请求、数据库中的用户可编辑字段、第三方接口或配置中心,就应该先进行明确的协议和目标校验,并尽量保持普通 string 类型。

看到 #ZgotmplZ 时按这个顺序排查

  1. 确认上下文。检查插值是否位于 href、src、CSS url() 或其它 URI 位置;同一个值放在普通文本节点里,处理方式可能不同。
  2. 打印原始值的来源。记录协议、主机和路径等非敏感字段,确认是不是大小写、前导空白、拼接或编码导致了意外的协议形态。
  3. 对照默认允许范围。相对 URL、http、https、mailto 可以进入模板过滤流程,其它协议不要假定会自动通过。
  4. 检查业务 allowlist。如果产品只允许站内地址,应在模板执行前拒绝外链;如果需要特殊协议,应让可信代码生成固定值,并把例外范围写成单独的函数或类型边界。
  5. 最后检查类型。搜索是否有人把任意输入转换成 template.URL。如果有,先撤掉这个转换,再修正输入来源和业务校验。

结论:把“能渲染”与“应该允许”分开

html/template 拒绝某些 URL 协议,是因为动态值进入了需要安全解释的 URL 上下文。#ZgotmplZ 是可识别的安全兜底信号,不是网络层错误。实际工程里可以记住三句话:

  • 普通动态 URL 保持 string,让模板负责上下文转义。
  • 业务层单独限制协议、域名和路径,不把模板的默认允许范围当成产品策略。
  • 只有可信代码构造的固定内容才考虑 template.URL,绝不把它用于不可信输入。

相关问题

为什么同一个字符串放在文本节点里没有被替换?

文本节点不承担 URL 导航语义,模板主要进行 HTML 文本转义;放进 href 或 src 后,值还要经过 URI 上下文的协议过滤。

换成 text/template 能避免这个结果吗?

它可能不再输出 #ZgotmplZ,但也失去了 HTML 上下文自动处理带来的安全边界。生成 HTML 时应优先使用 html/template,不要为了保留某个协议而换到更弱的模板包。

协议被允许后还需要限制域名吗?

需要。协议过滤只回答“这个 URI 方案是否进入 HTML”,并不回答“业务是否允许跳到这个主机或路径”。开放跳转、外链策略和站内路径都应由业务层单独决定。

参考资料:Go 官方 html/template 包文档:https://pkg.go.dev/html/template;URL 过滤源码:https://go.dev/src/html/template/url.go。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
text/template missingkey 设置不生效的原因text/template missingkey 设置不生效的原因
上一篇
text/template missingkey 设置不生效的原因
slog.Handler 怎样让 source 字段只在调试开启
下一篇
slog.Handler 怎样让 source 字段只在调试开启
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1010次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1014次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1139次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1111次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    955次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码