启动模板自检 + 站长导航放行 + 统计脚本关口收紧
排查:/admin/settings 渲染到一半就断
- 现象:页面在「统计脚本」那张卡片处截断,没有保存按钮、没有 </form></html>,而 HTTP 仍是 200。
- 根因:跑的是旧二进制(改动前编译),磁盘上的模板却是新的。debug=true 下 pageSet()
每次渲染都从磁盘重解析模板,于是新 settings.html 引用了旧 setting.Settings 里没有的
TrackEnabled —— 执行期报错,而 render 只把错误写进日志,客户端只看到半张页面。
- 判据:strings <二进制> | grep -c cleanTrackScript(旧 0 / 新 2)。别用 strings | grep
TrackScript —— config 里本来就有这个名字,旧二进制也命中,是个假信号。
启动自检(防复发)
- template_smoke.go:pageProbe 给 24 个页面各配一份最小可用数据,New() 里全部渲染一遍,
任一页失败就拒绝启动。pageProbe 与 pageFiles 双向校验,新加页面必须补探针,
漏配按失败处理(不是省事)。
- smokeOne 用 recover 兜住方法里的 panic,报「哪一页坏了」而不是崩栈。
- 实测:往 settings.html 插一个不存在的字段 → 启动即报
「模板自检未通过…admin/settings: can't evaluate field …」,退出码 1。
- 门禁 template_smoke_test.go:一条跑全量自检,一条变异验证(探针换成字段更少的类型
必须报错且点名该页)。
站长导航被页面开关误伤
- guardPage 只拦访客(站长始终能访问全部公开页),但导航写的是 {{if .Settings.HasPage "x"}},
没有 IsAdmin 分支 ⇒ 页面开关一关,站长自己也只剩「主页」一个入口 —— 能访问却点不到。
改成 {{if or .IsAdmin (.Settings.HasPage "x")}}(map/places/timeline/stats/report 五处)。
- 门禁 nav_visibility_test.go:三向不变量(站长仍有全部入口 / 访客一个都没有 /
全开放时访客也看得到),做过变异验证。
- 访客侧「只有主页」是按后台配置应有的状态,没有动数据。
统计脚本关口收紧(cleanTrackScript)
- src= 只在 <script …> 开标签内部找。原来对整段做 strings.Index,脚本体里一句
`img.src=u` 就被命中、整段被判危险而清空 —— 页面照样提示「设置已保存」,站长看不出没挂上。
- 开闭标签从「只拒 shut > open」改成严格配平:末尾未闭合的 <script> 会把页面收尾整段吞进脚本。
- 不再静默:签名改成 (script, warn),说明码经 ?warn= 回显成人话;判为危险时整段不采纳、
库里原脚本与开关都不动(把站长手上好用的脚本一并清掉更糟)。
- 门禁 TestCleanTrackScript 重写:逐条边界 + 定义性不变量(采纳了非空结果时,
里面开闭标签数必定相等)+ TestTrackWarnMessage(未识别码不得回显)。变异验证做过。
- 文案订正:settings.html 的「conf/app.ini 的 track_script 会作为这里的默认值」有歧义 ——
它不回填文本框,而是「这个框留空时、注入那一刻才回落到 ini」。
其余
- bin/ 目录删除(15 个 exe / 412MB),二进制只留仓库根 rond.exe;
技能 rond-web-verify 与 memory details 十八里 bin/rond.exe 的写法同步改掉。
- 验收:./scripts/verify.sh 全绿且 0 skip(PASS=346 FAIL=0 SKIP=0)。
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -467,6 +468,7 @@ func (s *Server) settingsPage(w http.ResponseWriter, r *http.Request) {
|
||||
d := SettingsData{Page: p, Modules: setting.AllModules, Pages: setting.AllPages,
|
||||
MapInfo: s.mapInfo(), MapLayers: layers, S3Info: s.s3Info()}
|
||||
d.Flash = r.URL.Query().Get("saved")
|
||||
d.Warn = trackWarnMessage(r.URL.Query().Get("warn"))
|
||||
s.render(w, "admin/settings", d)
|
||||
}
|
||||
|
||||
@@ -581,57 +583,105 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.PostFormValue("regions_mode") == setting.RegionsBlacklist {
|
||||
next.RegionsMode = setting.RegionsBlacklist
|
||||
}
|
||||
next.TrackScript = cleanTrackScript(r.PostFormValue("track_script"))
|
||||
// 开关只在「勾选 + 脚本非空」时才生效:勾了但没填内容等于没开,
|
||||
// 否则 layout 里会挂一个空 <script>,看着像开了其实什么也没统计。
|
||||
next.TrackEnabled = r.PostFormValue("track_enabled") == "on" && next.TrackScript != ""
|
||||
script, trackWarn := cleanTrackScript(r.PostFormValue("track_script"))
|
||||
if trackWarnRejected(trackWarn) {
|
||||
// 判为危险:这次改动整段不采纳,库里原有的脚本与开关都保持不动
|
||||
// (next := cur 已经带上了它们)。把站长手上本来好用的脚本一并清掉,
|
||||
// 比拒绝这次改动更糟。
|
||||
} else {
|
||||
next.TrackScript = script
|
||||
// 开关只在「勾选 + 脚本非空」时才生效:勾了但没填内容等于没开,
|
||||
// 否则 layout 里会挂一个空 <script>,看着像开了其实什么也没统计。
|
||||
next.TrackEnabled = r.PostFormValue("track_enabled") == "on" && next.TrackScript != ""
|
||||
}
|
||||
if err := s.sets.Save(ctx, next); err != nil {
|
||||
s.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/settings?saved=1", http.StatusSeeOther)
|
||||
dest := "/admin/settings?saved=1"
|
||||
if trackWarn != "" {
|
||||
dest += "&warn=" + url.QueryEscape(trackWarn)
|
||||
}
|
||||
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// cleanTrackScript 收拾管理员粘进来的统计脚本。
|
||||
// 统计脚本没被原样采纳时的三类说明码。它们是固定常量、不是用户输入,
|
||||
// 所以可以安全地放进跳转查询串,再由 trackWarnMessage 翻成人话。
|
||||
const (
|
||||
warnTrackTruncated = "truncated"
|
||||
warnTrackUnbalanced = "unbalanced"
|
||||
warnTrackBadSrc = "badsrc"
|
||||
)
|
||||
|
||||
// trackWarnMessage 把说明码翻成人话;未识别的码不提示(不回显查询串原文)。
|
||||
func trackWarnMessage(code string) string {
|
||||
switch code {
|
||||
case warnTrackTruncated:
|
||||
return "统计脚本超过 4096 字符,已截断保存"
|
||||
case warnTrackUnbalanced:
|
||||
return "统计脚本里 <script> 与 </script> 的数量不配对,本次改动已拒绝,库里原有脚本保持不变"
|
||||
case warnTrackBadSrc:
|
||||
return "统计脚本的 src 只接受 http://、https:// 或站内相对路径,本次改动已拒绝,库里原有脚本保持不变"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// trackWarnRejected 报告这个说明码是否代表「整段没被采纳」。
|
||||
func trackWarnRejected(code string) bool {
|
||||
return code == warnTrackUnbalanced || code == warnTrackBadSrc
|
||||
}
|
||||
|
||||
// cleanTrackScript 收拾管理员粘进来的统计脚本,返回采纳后的内容与一个说明码(空 = 原样通过)。
|
||||
//
|
||||
// 这段内容会被原样注入**每一个公开页面**,所以要挡住几类会伤到访客的输入。
|
||||
// 但**不能**简单地把「出现 </script 就拒」当规则:百度统计、Umami 这类服务
|
||||
// 贴进来的就是完整 HTML,一行里必然带 </script>,一拒就把正常用法挡死了。
|
||||
//
|
||||
// 真正的注入面只有一种:**未闭合的 <script> 里出现 </script> 会提前闭合**,
|
||||
// 后面的内容当正文渲染,页面布局散架。判据因此是「奇偶」:
|
||||
// 开标签数与闭合标签数不配平 ⇒ 有提前闭合 ⇒ 拒。配平则整段原样放行。
|
||||
// 两条判据:
|
||||
// - 开闭 <script> 标签必须**严格配平**:少一个 </script> 会提前闭合、后面的内容
|
||||
// 当正文渲染(页面布局散架),多一个 <script> 则会把页面剩下的收尾整段吞进脚本。
|
||||
// - 开标签上的 src 只放行 http(s):// 与站内相对路径,挡掉 javascript: / data: 伪协议。
|
||||
//
|
||||
// 另外挡住:javascript: / data: 这类伪协议的 src(点击即执行),
|
||||
// 以及超长内容(多半是误粘了整份 HTML 文档,截断)。
|
||||
func cleanTrackScript(v string) string {
|
||||
// src 只在**标签内部**找。以前是对整段做 strings.Index(low, "src="),于是脚本体里
|
||||
// 一句 `img.src=u` 就会被命中、整段被判危险而清空——页面却照样提示「设置已保存」,
|
||||
// 站长根本看不出脚本没挂上(实测复现过)。
|
||||
func cleanTrackScript(v string) (script, warn string) {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return ""
|
||||
return "", ""
|
||||
}
|
||||
const maxLen = 4096
|
||||
if len(v) > maxLen {
|
||||
v = v[:maxLen]
|
||||
warn = warnTrackTruncated
|
||||
}
|
||||
low := strings.ToLower(v)
|
||||
// 开闭标签数必须配平,否则有人能靠 </script> 提前闭合、把后面的内容
|
||||
// 从「脚本」变成「正文」注入到页面里。
|
||||
open := strings.Count(low, "<script")
|
||||
// 去掉属性里的 <script 字样不算开标签(如 data-src="<script"),这里只做保守计数:
|
||||
// 宁可多拒一次,也别放过真的注入。
|
||||
shut := strings.Count(low, "</script")
|
||||
if shut > open {
|
||||
return ""
|
||||
}
|
||||
// src 只放行 http/https 与站内相对路径,挡掉 javascript: / data: 伪协议
|
||||
if idx := strings.Index(low, "src="); idx >= 0 {
|
||||
rest := strings.TrimLeft(v[idx+4:], "\"'")
|
||||
if rest != "" && !strings.HasPrefix(rest, "http://") &&
|
||||
!strings.HasPrefix(rest, "https://") && !strings.HasPrefix(rest, "/") {
|
||||
return ""
|
||||
open := 0
|
||||
for i := 0; ; {
|
||||
j := strings.Index(low[i:], "<script")
|
||||
if j < 0 {
|
||||
break
|
||||
}
|
||||
start := i + j
|
||||
open++
|
||||
end := strings.IndexByte(low[start:], '>')
|
||||
if end < 0 {
|
||||
break // 这个开标签没闭合,交给下面的配平判定处理
|
||||
}
|
||||
tag := low[start : start+end]
|
||||
if k := strings.Index(tag, "src="); k >= 0 {
|
||||
rest := strings.TrimLeft(tag[k+4:], "'\" \t")
|
||||
if rest != "" && !strings.HasPrefix(rest, "http://") &&
|
||||
!strings.HasPrefix(rest, "https://") && !strings.HasPrefix(rest, "/") {
|
||||
return "", warnTrackBadSrc
|
||||
}
|
||||
}
|
||||
i = start + end + 1
|
||||
}
|
||||
return v
|
||||
if shut := strings.Count(low, "</script"); open != shut {
|
||||
return "", warnTrackUnbalanced
|
||||
}
|
||||
return v, warn
|
||||
}
|
||||
|
||||
// ---------- 修改口令 ----------
|
||||
|
||||
Reference in New Issue
Block a user