安全加固 + 结构清理:修注入/串写/竞态,DOMPurify 上线,后端补事务与 handler 测试

后端:
- ORDER BY 白名单(sanitizeOrder)堵住 ?order= SQL 注入,补回归测试
- 登录限速(每 IP 10 次失败/10 分钟 429)、TLS/反代下 Secure cookie、NewAPI 构造器
- Delete/setTags/MergeTags/DeleteTag 包事务;Archive 去 500 篇上限
- 列表接口裁剪:不传 content_md,长文 content_html 截 600,新增 content_len;health 探 DB

前端:
- EditorView 路由复用串写修复(RouterView :key + sync watch 回写原文章)
- v-html 出口统一过 DOMPurify(sanitizeHtml),stripTags 改 DOMParser
- 列表竞态防护(Home/Tag/Posts 请求序号)、TagView 分页修复
- 侧栏接口 30s 缓存去重;one:unauthorized 监听器泄漏修复
- 删 styles.css 498 行重复块;移除 tailwind/marked/vue-tsc 死依赖;CommandPalette a11y 语义
This commit is contained in:
Sakurasan
2026-09-21 23:59:09 +08:00
parent c762f06cd7
commit dd2994189a
25 changed files with 539 additions and 1411 deletions
+29
View File
@@ -8,6 +8,7 @@ import (
"errors"
"net/http"
"strings"
"sync"
"oneblog/internal/config"
"oneblog/internal/httpx"
@@ -19,6 +20,25 @@ type API struct {
Store *store.Store
Cfg *config.Config
Sessions *Sessions
loginOnce sync.Once
logins *loginLimiter
}
func NewAPI(st *store.Store, cfg *config.Config, sessions *Sessions) *API {
a := &API{Store: st, Cfg: cfg, Sessions: sessions}
a.limiter()
return a
}
// limiter 惰性初始化,兼容测试里的 &API{...} 零值构造。
func (a *API) limiter() *loginLimiter {
a.loginOnce.Do(func() {
if a.logins == nil {
a.logins = newLoginLimiter()
}
})
return a.logins
}
const cookieName = "one_session"
@@ -81,6 +101,11 @@ func (a *API) login(w http.ResponseWriter, r *http.Request) {
httpx.Error(w, http.StatusMethodNotAllowed, "POST required")
return
}
key := sourceKey(r)
if a.limiter().blocked(key) {
httpx.Error(w, http.StatusTooManyRequests, "失败次数过多,请 10 分钟后再试")
return
}
var in loginRequest
if err := httpx.Decode(r, &in); err != nil {
httpx.BadRequest(w, "invalid body")
@@ -89,15 +114,18 @@ func (a *API) login(w http.ResponseWriter, r *http.Request) {
userOK := subtle.ConstantTimeCompare([]byte(in.Username), []byte(a.Cfg.AdminUser)) == 1
passOK := subtle.ConstantTimeCompare([]byte(in.Password), []byte(a.Cfg.AdminPass)) == 1
if !userOK || !passOK {
a.limiter().fail(key)
httpx.Unauthorized(w)
return
}
a.limiter().reset(key)
token, exp := a.Sessions.Issue(a.Cfg.AdminUser)
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: token,
Path: "/",
HttpOnly: true,
Secure: isTLS(r),
SameSite: http.SameSiteLaxMode,
Expires: exp,
MaxAge: a.Sessions.TTL(),
@@ -113,6 +141,7 @@ func (a *API) logout(w http.ResponseWriter, r *http.Request) {
Value: "",
Path: "/",
HttpOnly: true,
Secure: isTLS(r),
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
})