Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions apps/admin/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -1567,10 +1567,16 @@ async def update_config(self, data: dict):
detail="storage_limit 不能小于 0",
)

try:
validate_background_url(next_config.get("background", ""))
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
# 只校验"发生变化"的值:升级前存入的旧格式 background(相对路径、含空格
# 或括号)在旧版本是合法的,若每次保存都重新校验,存量部署会连无关设置项
# 都保存不了(一律 400)。渲染侧仍然 html 转义,而任何修改都必须通过校验。
current_background = str(settings.background or "")
candidate_background = str(next_config.get("background") or "")
if candidate_background != current_background:
try:
validate_background_url(candidate_background)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))

if admin_password_changed:
next_config["jwt_secret"] = generate_jwt_secret()
Expand Down
4 changes: 3 additions & 1 deletion core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ def verify_password(password: str, hashed: str) -> bool:
p=int(p),
maxmem=_SCRYPT_MAXMEM,
).hex()
except (ValueError, TypeError):
# 默认解释器对超范围的 n/r/p 抛 TypeError/ValueError;部分构建会抛
# OverflowError,一并视为校验失败,避免坏掉的存量哈希影响登录路径。
except (ValueError, TypeError, OverflowError):
return False
return hmac.compare_digest(password_hash, stored_hash)

Expand Down
58 changes: 58 additions & 0 deletions tests/test_background_url_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,63 @@ async def _scenario(self):
await close_db()


class LegacyBackgroundUpgradeTests(SettingsOverrideMixin, unittest.TestCase):
"""存量部署升级:旧版本合法、新校验不接受的值,不得阻塞设置保存。"""

def test_unchanged_legacy_background_does_not_block_unrelated_save(self):
asyncio.run(self._unchanged_legacy_allows_save())

async def _unchanged_legacy_allows_save(self):
await init_memory_db()
try:
from apps.base.config import refresh_settings
from apps.base.models import KeyValue

# 旧版本合法(相对路径 + 空格),新的 http(s) 校验会拒绝
legacy = "/static/bg image.png"
await KeyValue.create(
key="settings", value={"background": legacy, "name": "old"}
)
await refresh_settings(force=True)

service = ConfigService()
# 修复前这里会 400,导致存量部署连无关设置项都保存不了
await service.update_config({"name": "new"})

record = await KeyValue.filter(key="settings").first()
self.assertEqual(record.value.get("background"), legacy, "旧值应原样保留")
self.assertEqual(record.value.get("name"), "new")
finally:
await close_db()

def test_changing_background_is_still_validated(self):
asyncio.run(self._change_still_validated())

async def _change_still_validated(self):
await init_memory_db()
try:
from fastapi import HTTPException

from apps.base.config import refresh_settings
from apps.base.models import KeyValue

await KeyValue.create(key="settings", value={"background": "/legacy/bg.png"})
await refresh_settings(force=True)

service = ConfigService()
# 修改为恶意值仍然被拒
with self.assertRaises(HTTPException) as ctx:
await service.update_config(
{"background": "x') ;background:url(https://evil.com/)"}
)
self.assertEqual(ctx.exception.status_code, 400)
# 修改为合法值可以通过
await service.update_config({"background": "https://ok.example/bg.png"})
record = await KeyValue.filter(key="settings").first()
self.assertEqual(record.value.get("background"), "https://ok.example/bg.png")
finally:
await close_db()


if __name__ == "__main__":
unittest.main()
5 changes: 5 additions & 0 deletions tests/test_password_hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ def test_malformed_hashes_are_rejected_not_crashing(self):
self.assertFalse(verify_password("x", "scrypt$n$r$p$salt$zzz"))
self.assertFalse(verify_password("x", "sha256$only-two"))
self.assertFalse(verify_password("x", ""))
# 超范围的 n/r/p:解释器抛 TypeError/ValueError(部分构建抛 OverflowError),
# 都必须被吞掉并判为不匹配,而不是让登录及每个请求 500
self.assertFalse(
verify_password("x", "scrypt$99999999999999999999999999$8$1$aa$bb")
)


class TransparentRehashTests(SettingsOverrideMixin, unittest.TestCase):
Expand Down
Loading