@@ -196,6 +196,82 @@ def test_security_headers_present(app):
196196 assert response .headers ["content-security-policy" ] == "frame-ancestors 'none'"
197197
198198
199+ def test_hsts_only_for_https_deployments (tmp_path ):
200+ """HSTS 仅 https 部署下发:明文场景发了无意义,还会预锁本地 http 访问。"""
201+ https_app = build_app (Settings (
202+ _env_file = None , APP_SECRET = SECRET , DATA_DIR = str (tmp_path ),
203+ ADMIN_USERNAMES = "root" , PUBLIC_BASE_URL = "https://gw.example.com" ))
204+ with TestClient (https_app ) as client :
205+ assert client .get ("/health" ).headers ["strict-transport-security" ] \
206+ .startswith ("max-age=" )
207+
208+
209+ def test_hsts_absent_for_http_deployment (app ):
210+ _app , client = app
211+ assert "strict-transport-security" not in client .get ("/health" ).headers
212+
213+
214+ # ------------------------------------------------------------ 文档端点开关
215+
216+
217+ def test_docs_disabled_by_default (app ):
218+ """默认不开 docs:openapi schema 绝不对外(/docs 与 /openapi.json 由
219+ SPA catch-all 接住返回前端壳,无 API 结构泄漏)。"""
220+ _app , client = app
221+ for path in ("/docs" , "/openapi.json" ):
222+ response = client .get (path )
223+ assert response .status_code == 200
224+ assert "text/html" in response .headers ["content-type" ]
225+ assert '"openapi"' not in response .text
226+
227+
228+ def test_docs_opt_in (tmp_path ):
229+ application = build_app (Settings (
230+ _env_file = None , APP_SECRET = SECRET , DATA_DIR = str (tmp_path ),
231+ ADMIN_USERNAMES = "root" , ENABLE_DOCS = True ))
232+ with TestClient (application ) as client :
233+ assert client .get ("/docs" ).status_code == 200
234+ assert client .get ("/openapi.json" ).status_code == 200
235+
236+
237+ # ------------------------------------------------------------ 登录入口加固
238+
239+
240+ def test_login_ip_respects_trusted_proxy (tmp_path ):
241+ """反代部署(trust_proxy=true)下,登录审计按 XFF 最后条目取来源 IP;
242+ 限流桶因此按真实客户端分桶,而不是全站共享代理地址一个桶。"""
243+ from src .audit .actions import ACTION_LOGIN_FAILURE
244+ from src .db .repo import AuditRepository
245+
246+ application = build_app (Settings (
247+ _env_file = None , APP_SECRET = SECRET , DATA_DIR = str (tmp_path ),
248+ ADMIN_USERNAMES = "root" , TRUST_PROXY = True ))
249+ headers = {"X-Forwarded-For" : "203.0.113.7, 198.51.100.9" }
250+ with TestClient (application ) as client :
251+ assert client .post ("/api/auth/login" ,
252+ json = {"username" : "root" , "password" : "bad" },
253+ headers = headers ).status_code == 401
254+ audit = AuditRepository (application .state .services .audit ._db )
255+ rows = audit .query (action = ACTION_LOGIN_FAILURE , limit = 1 )
256+ assert rows and rows [0 ]["ip" ] == "198.51.100.9"
257+
258+
259+ def test_login_csrf_rejected_with_session_cookie_and_cross_origin (admin_client ):
260+ """login CSRF:带会话 cookie 的跨站登录请求一并拦截;无 cookie 的
261+ 登录(curl / 首次登录)不受影响(由 csrf_protected 的跳过逻辑保证)。"""
262+ response = admin_client .post (
263+ "/api/auth/login" , json = {"username" : "root" , "password" : "bad" },
264+ headers = {"Origin" : "http://evil.example.com" })
265+ assert response .status_code == 403
266+
267+
268+ def test_login_without_cookie_skips_csrf (app ):
269+ _app , client = app
270+ # 无会话 cookie:非浏览器客户端直接放行到密码校验(401 而非 403)
271+ assert client .post ("/api/auth/login" ,
272+ json = {"username" : "root" , "password" : "bad" }).status_code == 401
273+
274+
199275# ------------------------------------------------------------- Host 白名单
200276
201277
0 commit comments