安全加固:Web 登录防爆破(IP+用户名双维度限速 5次/分钟锁10分钟,未知用户名 dummy bcrypt 防枚举),瓦片代理 SSRF 加固(DialContext 拒绝内网/链路本地/CGNAT/ULA 与重定向限制,Content-Type image 白名单+nosniff),/api/discard-details 公开去敏(新增 admin 全字段端点),公开地图源接口外部 URL/key 一律代理化,install.sh 随机管理员密码+非回环默认口令拒绝启动,后端 v1.4.0

This commit is contained in:
2026-08-20 17:00:41 +08:00
parent fe0cf036cf
commit f21e8337af
13 files changed
+658 -48

No files matched your search

+10 -3
View File
@@ -5,6 +5,7 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -163,11 +164,11 @@ func AdminDTO(row storepkg.MapTileSourceRecord) gin.H {
return gin.H{"id": row.ID, "name": row.Name, "url_template": row.URLTemplate, "attribution": row.Attribution, "max_zoom": row.MaxZoom, "enabled": row.Enabled, "is_default": row.IsDefault, "proxy_enabled": row.ProxyEnabled, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt}
}
// PublicDTO 是给前端用户使用的视图:当 ProxyEnabled 为 true 时,url 改写为
// 通过本服务的 /api/map/{hash} 代理路径避免暴露上游瓦片地址。
// PublicDTO 是给前端用户使用的视图:外部 http(s) 模板一律改写为经本服务的
// /api/map/{hash} 代理路径,避免向下游暴露上游瓦片地址与密钥
func PublicDTO(row storepkg.MapTileSourceRecord) gin.H {
urlTemplate := row.URLTemplate
if row.ProxyEnabled {
if isExternalTileURLTemplate(urlTemplate) {
hash := row.URLTemplateHash
if hash == "" {
hash = storepkg.MapTileSourceHash(row.URLTemplate)
@@ -176,3 +177,9 @@ func PublicDTO(row storepkg.MapTileSourceRecord) gin.H {
}
return gin.H{"id": row.ID, "name": row.Name, "url_template": urlTemplate, "attribution": row.Attribution, "max_zoom": row.MaxZoom}
}
// isExternalTileURLTemplate 判断模板是否指向外部 http/https 资源。
func isExternalTileURLTemplate(template string) bool {
t := strings.ToLower(strings.TrimSpace(template))
return strings.HasPrefix(t, "http://") || strings.HasPrefix(t, "https://")
}
@@ -0,0 +1,45 @@
package mapsource
import (
"testing"
storepkg "meshtastic_mqtt_server/internal/store"
)
func TestIsExternalTileURLTemplate(t *testing.T) {
for _, in := range []string{"http://tile.openstreetmap.org/x", "https://webst03.is.autonavi.com/appmaptile?key=1", "HTTPS://EXAMPLE.COM/x"} {
if !isExternalTileURLTemplate(in) {
t.Errorf("%q should be external", in)
}
}
for _, in := range []string{"/api/map/abc?x={x}", "", " /relative/path "} {
if isExternalTileURLTemplate(in) {
t.Errorf("%q should not be external", in)
}
}
}
func TestPublicDTOAlwaysProxiesExternalURL(t *testing.T) {
row := storepkg.MapTileSourceRecord{
URLTemplate: "https://tile.example.com/style=7&x={x}&y={y}&z={z}&key=SECRET_KEY",
}
dto := PublicDTO(row)
got, _ := dto["url_template"].(string)
want := "/api/map/" + storepkg.MapTileSourceHash(row.URLTemplate) + "?x={x}&y={y}&z={z}"
if got != want {
t.Errorf("url_template = %q, want %q", got, want)
}
if dto["url_template"].(string) == row.URLTemplate {
t.Error("external url_template must not be exposed")
}
}
func TestPublicDTOKeepsRelativeTemplate(t *testing.T) {
row := storepkg.MapTileSourceRecord{
URLTemplate: "/api/map/abc?x={x}&y={y}&z={z}",
}
dto := PublicDTO(row)
if got := dto["url_template"].(string); got != row.URLTemplate {
t.Errorf("relative template should stay as-is, got %q", got)
}
}