#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ theme.py 纯逻辑回归测试(不需要目标机、不需要数据库、不需要 root) 运行:python3 router-build/_dev/t-theme.py 覆盖: * 取值校验 valid_css_value(颜色 / 数值 / 阴影 / 渐变 / var() / 各种攻击串) * 主题结构校验 validate_theme(字段、长度、白名单、必填) * 自定义 CSS 安全检查 _check_extra_css(@ 规则、url()、注释、残缺语法) * 渲染 theme_to_css * 配色生成 make_palette / builtin_themes * ZIP 打包 → 解包往返 * ZIP 异常:坏包、无 manifest、路径穿越、超大文件、非法 JSON、非法主题 * id 生成 slugify / theme_id_from_name """ import sys import os import io import re import json import base64 import zipfile HERE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(os.path.dirname(HERE), 'backend') sys.path.insert(0, SRC) import theme # noqa: E402 PASS = FAIL = 0 MSG = [] def has(name, cond, extra=''): global PASS, FAIL if cond: PASS += 1 else: FAIL += 1 MSG.append(' ✗ %s%s' % (name, (' → ' + str(extra)) if extra else '')) def chk(name, hay, needle, want=True): has(name, (needle in hay) == want, hay[:160]) def expect_ok(name, r, want=True, needle=None): """断言主题校验结果 ok/not ok,可选的错误关键词""" if isinstance(r, tuple): ok_ = r[0] detail = r[1] if len(r) > 1 else '' else: ok_, detail = True, r has(name, ok_ == want, detail) if isinstance(r, tuple) and not want and needle: chk(name + ' (错误文案)', str(detail), needle) # ================================================================== print('== 1. valid_css_value:合法取值 ==') # ================================================================== GOOD = [ '#fff', '#FFF', '#ffff', '#1f6feb', '#1f6febcc', 'rgb(31,111,235)', 'rgba(31,111,235,.35)', 'rgba(0,0,0,0)', 'hsl(210,90%,55%)', 'hsla(210,90%,55%,0.5)', '12px', '1rem', '0', '2.5em', '50%', 'transparent', 'currentColor', 'white', 'steelblue', 'var(--pri)', 'var(--pri, #fff)', 'linear-gradient(135deg,#2b7bf3 0%,#1f6feb 45%,#1552c0 100%)', 'radial-gradient(circle,#fff,#000)', '0 1px 2px rgba(18,28,55,.05),0 2px 8px rgba(18,28,55,.05)', 'inset 0 1px 0 #fff', ] for g in GOOD: ok_, why = theme.valid_css_value(g) has('合法取值 %r' % g, ok_, why) print('== 2. valid_css_value:危险取值必须被拒 ==') BAD = [ ('#zzz', '非十六进制'), ('#12345', '5 位色'), ('rgb(300,0,0)', '超范围'), ('rgba(1,2,3)', '缺透明度第4位却写 rgba'), ('red;background:url(x)', '注入分号'), ('#fff}', '闭合花括号'), ('#fff{', '开花括号'), ('url(http://evil.com/x.css)', '外部资源'), ('expression(alert(1))', '表达式'), ('@import url(x)', 'at 规则'), ('#fff\\', '反斜杠'), ('/* */ #fff', '注释符号'), ('var(--not-opened)', '引用未开放变量'), ('var(', '残缺 var'), ('', '空值'), (None, 'None'), ('x' * 300, '超长'), ('#fff !important', 'important'), ('12vh', '?'), # 12vh 本身合法(只对部分变量限制),这里用作对照 ] for b, why_desc in BAD: if b == '12vh': # 通用校验允许 vh(仅 --r/--side/--side-c 禁止) ok_, why = theme.valid_css_value('12vh') has('12vh 通用校验允许', ok_, why) continue # rgba(1,2,3) 应该被允许(rgb 别名,alpha 可选) if b == 'rgba(1,2,3)': ok_, why = theme.valid_css_value('rgba(1,2,3)') has('rgba(1,2,3) 合法(alpha 可选)', ok_, why) continue ok_, why = theme.valid_css_value(b) has('拒绝危险取值 %r(%s)' % (b, why_desc), not ok_, '通过了,这是漏洞!') # 针对性:单位限制 for var in ('--r', '--side', '--side-c'): ok_, _ = theme.valid_css_value('12vw', var) has('%s 拒绝 vw' % var, not ok_) ok_, _ = theme.valid_css_value('12px', var) has('%s 接受 px' % var, ok_) # ================================================================== print('== 3. validate_theme:结构校验 ==') # ================================================================== ok_, t = theme.validate_theme({ 'name': '测试主题', 'vars': {'--pri': '#ff0000', '--bg': '#f5f5f5'}, }) has('最小合法主题通过', ok_, t) has('vars 被规范化', ok_ and t['vars']['--pri'] == '#ff0000') has('默认 version', ok_ and t['version'] == '1.0') # 缺名称 ok_, why = theme.validate_theme({'vars': {'--pri': '#fff'}}) has('缺 name 被拒', not ok_) chk('缺 name 文案', why, '主题名称') ok_, why = theme.validate_theme({'name': ''}) has('空 name 被拒', not ok_) # 非法名称字符 ok_, why = theme.validate_theme({'name': 'bad