Ruff 规则 S704unsafe-markup-use全解用静态检查拦截markupsafe.Markup的 XSS 隐患【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffRuffRust 编写的极速 Python linter通过 Bandit 兼容规则S704unsafe-markup-use检测向markupsafe.Markup传入非常量/非字面量内容的危险写法帮助你在 Web 模板与富文本渲染场景中提前消除跨站脚本XSS风险。本文以仓库内的 Markdown 测试文档 crates/ruff_linter/resources/mdtest/flake8-bandit/unsafe-markup-use.md 为骨架结合flake8_bandit插件源码与配置实现讲清该规则的检测边界、两个可调选项extend-markup-names、allowed-markup-calls的用法及其底层原理并给出可直接落地的配置与修复示例。S704 是什么一条稳定版安全规则unsafe-markup-use在规则体系中的代码是S704归属flake8-bandit规则组安全Security类别。在 crates/ruff_linter/src/codes.rs 中可以找到它的注册映射(Flake8Bandit, 704) rules::flake8_bandit::rules::UnsafeMarkupUse。规则本体定义在 crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs其ViolationMetadata标注了category Security与stable_since 0.10.0也就是说它自 Ruff 0.10.0 起即进入稳定规则集无需开启 preview 即可使用。检测命中时输出的诊断消息格式为Unsafe use of {name} detected其中name是实际被调用的完全限定名称例如markupsafe.Markup或flask.Markup。该消息模板由derive_message_formats派生见 unsafe_markup_use.rs。值得注意的是Ruff 曾经在自身规则RUF035下实现过同名检查0.10.0 起该规则被迁移并映射为 Bandit 的 S704removed_since 0.10.0旧实现现位于 crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs其文档注释明确写着This rule was implemented inbanditand has been remapped to S704。因此你直接在lint.select中启用 S704 即可获得最新行为。为什么危险Markup本身不做转义markupsafe.Markup是 Jinja2 / Flask 生态中用于标记安全 HTML的类型。关键点在于把字符串交给Markup()并不会执行任何转义——它只是把内容包装成可安全插入模板的标记对象。如果你把一个包含用户输入的 f-string、变量或插值字符串直接传进去结果会被当作已信任的 HTML原样输出从而可能造成 XSS 漏洞。规则文档注释给出了典型错误→正确对照# ❌ XSScontent 是用户可控内容未经转义直接进入 Markup from markupsafe import Markup content scriptalert(Hello, world!)/script html Markup(fb{content}/b) # ✅ 安全先构造 Markup 模板动态内容由 format() 转义后插入 from markupsafe import Markup content scriptalert(Hello, world!)/script html Markup(b{}/b).format(content)再比如用join拼接行内容时的等价问题# ❌ XSSlines 中混入未信任的纯字符串 from markupsafe import Markup lines [ Markup(bheading/b), scriptalert(XSS attempt)/script, ] html Markup(br.join(lines)) # ✅ 安全把已转义的 Markup 交给另一个 Markup 的 join from markupsafe import Markup lines [ Markup(bheading/b), scriptalert(XSS attempt)/script, ] html Markup(br).join(lines)修复思路可以概括为一句话永远让动态内容作为「参数」流向Markup对象已有的format()/join()等方法完成转义而不是在Markup(...)调用里做字符串插值。启用规则与基础行为验证在配置文件pyproject.toml或ruff.toml中通过lint.select启用lint.select [S704]mdtest 文档中每个 Python 代码块都用# error: [unsafe-markup-use]注释标注期望命中位置并用# snapshot: unsafe-markup-use引出实际诊断快照作为行为测试与文档双重的校验依据。开启规则后遇到下述最典型的场景会报错import flask from markupsafe import Markup, escape content scriptalert(Hello, world!)/script Markup(funsafe {content}) # S704将动态 f-string 传入 Markup对应诊断快照渲染为error[S704]: Unsafe use of markupsafe.Markup detected -- src/mdtest_snippet.py:5:1 | 5 | Markup(funsafe {content}) # snapshot: unsafe-markup-use | ^^^^^^^^^^^^^^^^^^^^^^^^^^^在同样通过字符串插值制造内容的各种姿势中mdtest 覆盖了如下矩阵代码结果flask.Markup(unsafe {}.format(content))命中 S704先 format 成非常量字符串再包装Markup(content)传入变量命中 S704flask.Markup(unsafe %s % content)命中 S704%格式化同样危险Markup(safe {}).format(content)安全字面量模板 转义插入flask.Markup(bsafe {}, encodingutf-8).format(content)安全字节字面量参数 编码关键字escape(content)安全escape()已完成转义与Markup无关Markup(objectsafe)安全未把任何动态内容经字符串插值引入这组用例共同揭示了实现中什么算不安全的判定口径只关心第一个位置参数且当该参数是普通字符串字面量、字节字面量或白名单调用时放行其余一律视为潜在 XSS 载体。判定的核心逻辑is_unsafe_call位于 unsafe_markup_use.rsmatches!(*call.arguments.args, [first] if !first.is_string_literal_expr() !first.is_bytes_literal_expr() !is_whitelisted_call(first, semantic, settings))源码注释同时说明了两点设计取舍之所以不追踪关键字参数是因为缺少类型推断时无法确定哪个关键字对应第一个位置参数而实际用关键字形式传内容的写法也很罕见未来若想放行带有__html__属性的动态值需要等类型系统层面的支持该属性是 MarkupSafe 公开 API 的一部分。已知边界目前检测不到与误报的两类场景S704 当前的实现并非完备的污点分析mdtest 文档对此有透明交代理解这些边界有助于你正确解读扫描结果。其一未被捕获的不安全用例。下面的写法同样不安全但当前版本检测不到Markup(objectunsafe {}.format(content))原因正如上文所述is_unsafe_call只审查位置参数列表call.arguments.args而此处危险内容通过object关键字参数传入属于实现层面的已知盲区。其二常量表达式造成的误报。以下两行其实并不包含用户输入但会被 S704 标记Markup(* * 8) # error: [unsafe-markup-use]误报 flask.Markup(hello {}.format(world)) # error: [unsafe-markup-use]误报mdtest 文档专门说明这类误报如果 Ruff 的类型检查器仓库内的ty子系统具备全面的常量表达式检测/常量求值能力未来有望消除。换句话说若你的代码里确实存在大量拿常量拼 HTML 字符串的用法需要权衡是接受少量误报、加# noqa豁免还是暂时使用lint.ignore中的 S704 配合其他规则组合。用extend-markup-names扩展类 Markup调用真实项目里你可能会用到第三方库里与markupsafe.Markup行为等价、同样不做转义的包装器例如 WebHelpers 2 的webhelpers.html.literal。选项lint.flake8-bandit.extend-markup-names就是为此设计的向 S704 追加应当按 Markup 对待的可调用对象名单。配置示例来自 mdtestlint.select [S704] lint.flake8-bandit.extend-markup-names [webhelpers.html.literal]配置后以下两种调用都会命中 S704from markupsafe import Markup from webhelpers.html import literal content scriptalert(Hello, world!)/script Markup(funsafe {content}) # error: [unsafe-markup-use] literal(funsafe {content}) # error: [unsafe-markup-use]选项要求完全限定名称crates/ruff_workspace/src/options.rs中 extend-markup-names 的选项文档 明确说明它接收的是形如webhelpers.html.literal、my_package.Markup的 fully-qualified 点分名称而非短名literal。默认值为[]类型为list[str]。对应实现见 flake8_bandit/settings.rs 中Settings结构体字段extend_markup_names默认空向量。mdtest 特别用一个用例守护了模块门控优化不产生误放行即便代码中没有 importmarkupsafe或flask正常门控路径会因此直接跳过检查只要配置了扩展名扩展名所指的调用仍必须被检查from webhelpers.html import literal content scriptalert(Hello, world!)/script literal(funsafe {content}) # error: [unsafe-markup-use]这里对应的源码就是unsafe_markup_call开头的门控判断unsafe_markup_use.rsif checker .settings() .flake8_bandit .extend_markup_names .is_empty() !(checker.semantic().seen_module(Modules::MARKUPSAFE) || checker.semantic().seen_module(Modules::FLASK)) { return; // 优化未配置扩展名、又没 import markupsafe/flask 时直接跳过 }即只有当extend_markup_names为空且源码里没有出现过markupsafe/flask模块时才提前返回一旦配置了扩展名即便不 import 这两个模块检查也会继续执行。用allowed-markup-calls声明输出安全的调用反过来有些函数如bleach.clean返回的是已经过净化/转义的可靠内容把它们传给Markup是常见且安全的设计。为避免这类场景被误报可用lint.flake8-bandit.allowed-markup-calls将其加入白名单lint.select [S704] lint.flake8-bandit.allowed-markup-calls [bleach.clean]启用后下面的用法不再报错from bleach import clean from markupsafe import Markup content scriptalert(Hello, world!)/script Markup(clean(content)) # 放行clean 的返回值被视为已净化与extend-markup-names一致白名单同样要求完全限定名例如bleach.clean而非clean。仓库中 allowed-markup-calls 的选项文档 还给出了更多适用场景提示不推荐但可行的是用它豁免 i18n 翻译函数安全性取决于实现与翻译内容的审计程度另一种常见用途是包装标记生成类函数的输出例如xml.etree.ElementTree.tostring或模板渲染引擎的结果——只要其中用户输入的净化在渲染前已经完成。从实现上看白名单生效于is_whitelisted_callunsafe_markup_use.rs它要求被检查的表达式确实是一个ExprCall解析其函数名的限定名并与allowed_markup_calls中每一项比对。因此存在一个 mdtest 明确标注的当前限制——不支持间接赋值from bleach import clean from markupsafe import Markup content scriptalert(Hello, world!)/script Markup(clean(content)) # 放行 cleaned clean(content) Markup(cleaned) # error: [unsafe-markup-use]间接赋值暂无法追踪也就是说白名单只作用于Markup(some_safe_call(...))直接嵌套这一形态一旦净化结果先存入变量再传入MarkupS704 会照常告警。从源码看完整判定链路S704 的入口函数是unsafe_markup_call它作为 AST 检查器的一部分在对函数调用表达式做语义分析时被触发调用点在 crates/ruff_linter/src/checkers/ast/analyze/expression.rs。整个判定流程可概括为四步模块门控extend_markup_names为空且未 importmarkupsafe/flask时直接返回性能优化见上文危险参数判定is_unsafe_call仅当第一个位置参数既非字符串/字节字面量、也非白名单调用时为不安全调用方身份解析通过resolve_qualified_name还原被调函数如flask.Markup、markupsafe.Markup、webhelpers.html.literal的完全限定名Markup 判定is_markup_call限定名匹配内置的[markupsafe | flask, Markup]二元组或命中extend_markup_names中任一项命中即上报诊断。其中步骤 4 的匹配实现unsafe_markup_use.rs同时解释了为什么extend-markup-names必须填全限定名——它内部正是通过QualifiedName::from_dotted_name把每个配置项转换成限定名后再与解析结果做等值比较的。规则文档注释还说明S704 的设计最初受 flake8-markupsafe 启发但默认不豁免任何 i18n 相关调用这与部分同类检查器的默认行为不同需要豁免时请显式使用allowed-markup-calls。落地建议在pyproject.toml或ruff.toml中启用[tool.ruff.lint] select [S704] [tool.ruff.lint.flake8-bandit] extend-markup-names [webhelpers.html.literal] allowed-markup-calls [bleach.clean]pyproject.toml中采用[tool.ruff.lint.flake8-bandit]段与 mdtest 里简写形式的lint.flake8-bandit.…对应的是同一组键选段及键名与仓库ruff.schema.json中定义的配置结构一致。修复优先把动态内容放进 format/join 参数把Markup(f...{x}...)改写成Markup(...{}...).format(x)确认净化先行的场景如bleach.clean、可控的渲染函数输出加入allowed-markup-calls确认自身即为不转义包装器的第三方对象如webhelpers.html.literal加入extend-markup-names审阅告警时留意两类边界经object等关键字参数传入的插值串当前无法捕获纯常量表达式构造的Markup字符串可能误报——前者建议配合代码评审后者可等待仓库类型检查子系统具备常量求值能力后由规则自动收敛。参考与延伸阅读本文用例与快照全部取自 crates/ruff_linter/resources/mdtest/flake8-bandit/unsafe-markup-use.md它同时充当规则行为测试规则实现见 crates/ruff_linter/src/rules/flake8_bandit/rules/unsafe_markup_use.rs选项默认值与解析见 crates/ruff_linter/src/rules/flake8_bandit/settings.rs 与 crates/ruff_workspace/src/options.rs旧版 RUF035 到 S704 的迁移记录可对照 crates/ruff_linter/src/rules/ruff/rules/unsafe_markup_use.rs 中的removed标注。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考