Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

security(expr): $F.mul/extend 等容器/字符串多态算子无界放大——29 字节表达式分配 1GB(@flow 源码 a*b 即编译成 $F.mul),$F.pow 的 DoS 守卫未推广

Closed
#68 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
50/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
backend, security

Research direction

Start with _fn_pow and its guard in plaita/core/expression.py, then _fn_mul, _fn_extend and _fn_join in the same file, and read the default registry where they are registered. Add a size check to _fn_mul for string and list repetition, and to the concatenation functions, raising ValueError like the pow guard. Run tests/unit/test_user_review_fixes.py, including test_pow_exponent_capped. Done when _fn_mul('A', 1000000000) raises ValueError, small results such as _fn_mul('ab', 3) are unchanged, and the existing tests still pass.

Written by the indexing model from the issue text.

Description

优先级 P1 · 依赖:无

基线 HEAD = 65f48f2

Summary

表达式引擎的数学/容器算子对「小输入、巨大输出」的多态放大没有任何规模守卫。$F.pow 在
37c2c62 已被加上指数上限(同批安全评审 P1),但 mul / add / extend / append /
join 等同属无界放大的算子完全没有同类保护,求值侧也不做返回值规模探测。

  1. plaita/core/expression.py:161-165 —— add / sub / mul / div / mod 直通 Python 算子,
    无任何规模检查;注意 mul 同时是多态算子:字符串重复、列表重复、数值乘法都走它:
def _fn_add(a, b): return a + b
def _fn_sub(a, b): return a - b
def _fn_mul(a, b): return a * b
def _fn_div(a, b): return a / b
def _fn_mod(a, b): return a % b
  1. plaita/core/expression.py:166-180 —— 唯一的守卫只覆盖 pow,且注释明写这里的风险模型
    (「约 20 字节的表达式曾可烧掉分钟级 CPU 并膨胀数十 MB 内存」),证明同类风险已被承认:
_POW_MAX_EXPONENT = 10_000


def _fn_pow(a, b):
    """幂运算,指数设上限(2026-09 安全评审 P1)。

    ``$F.pow(9, 9999999)`` 这种约 20 字节的表达式曾可烧掉分钟级 CPU 并膨胀
    数十 MB 内存——单流程即可拖死 worker,并行分支还能线性放大。
    """
    if isinstance(b, int) and abs(b) > _POW_MAX_EXPONENT:
        raise ValueError(
            f"$F.pow exponent too large: {b} (limit {_POW_MAX_EXPONENT}). "
            "Refusing to burn CPU on an unbounded power computation."
        )
    return a ** b
  1. plaita/core/expression.py:221-222 —— 容器侧同类算子同样裸奔:
def _fn_append(a, b): return a + [b]
def _fn_extend(a, b): return a + b
  1. plaita/core/expression.py:285 与 :337 —— 两者都进默认注册表,对任意 flow 的任意表达式可见:
        ("mul", _fn_mul, "Multiply two values"),
        ...
        ("extend", _fn_extend, "Extend list (returns new list)"),
  1. plaita/core/expression_parser.py:436-460 —— 求值侧直接 func(*args),不探测参数规模也不校验
    返回值规模,所以放大发生在 Python 字节码内部:
        def thunk(frame: Frame) -> Any:
            _context, registry, _prefix = frame
            args = [arg(frame) for arg in arg_thunks]
            logger.debug("parse_function: func_name=%s, args=%s", func_name, args)
            func = _lookup_function(registry, func_name)
            ...
            return func(*args)
  1. plaita/dsl/codeflow/_expr.py:36-43 —— 这不是「只能手写表达式才踩」的问题:@flow 源码里
    任意二元 * 都被编译成 $F.mul(...)(_BINOP_TO_F 在 plaita/dsl/codeflow/_common.py:92-95
    把 ast.Mult 映射为 mul):
    if isinstance(node, ast.BinOp):
        fname = _BINOP_TO_F.get(type(node.op))
        if fname is None:
            raise _CodeflowError(f"不支持的二元运算 {type(node.op).__name__}", node)
        left = _compile_expr(node.left, ctx)
        right = _compile_expr(node.right, ctx)
        return f"$F.{fname}({_render_arg(left)}, {_render_arg(right)})"

复现(实跑,/tmp/plaita-audit-venv/bin/python,Python 3.12,plaita editable 安装):

$ cd /tmp && /tmp/plaita-audit-venv/bin/python - <<'EOF'
import resource, time
from plaita.core.expression_parser import ExpressionParser
p = ExpressionParser.for_prefix('$')
ctx = {'$INPUT': {'lst': [0]}}
def rss(): return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024/1024
e = "{% $F.mul('A', 1000000000) %}"
print('expr len:', len(e))
t=time.time(); r=p.evaluate(e, ctx)
print('1e9 str len', len(r), 'secs %.3f'%(time.time()-t), 'rss MB %.1f'%rss())
t=time.time(); r2=p.evaluate('{% $F.mul($INPUT.lst, 100000000) %}', ctx)
print('list x1e8 len', len(r2), 'secs %.3f'%(time.time()-t), 'rss MB %.1f'%rss())
try: p.evaluate('{% $F.pow(9, 9999999) %}', ctx)
except ValueError as ex: print('pow guard:', ex)
EOF
--- 输出 ---
expr len: 29
1e9 str len 1000000000 secs 0.060 rss MB 1035.6
list x1e8 len 300000000 secs 1.977 rss MB 2084.7
pow guard: $F.pow exponent too large: 9999999 (limit 10000). Refusing to burn CPU on an unbounded power computation.

29 字节的表达式在 0.06 秒内产出 10 亿字符、RSS 冲到约 1GB;把 $INPUT.lst(长度 1 的单元素
列表)乘 1e8 得到长度 3 亿的列表、RSS 约 2GB。同一进程里 $F.pow(9, 9999999) 确实被守卫拦住,
即守卫只覆盖 pow 是确认过的现状,不是推测。

@flow 源码侧的可达性(实跑):

$ cd /Users/jeff.jie/projects/infra4agent/plaita && /tmp/plaita-audit-venv/bin/python -c \
  "from plaita.dsl.codeflow import compile_source; ir=compile_source('from plaita.dsl.codeflow import flow\n@flow(\"t\")\ndef t(INPUT):\n    big = \"A\" * 1000000000\n    return big\n'); print([n.get('output') for n in ir['nodes'] if n.get('type')=='assignment'])"
--- 输出 ---
['$F.mul("A", 1000000000)']

即普通 @flow 写法 big = "A" * 1000000000 编出来的 IR 就是 $F.mul("A", 1000000000),
不需要作者手写 {% ... %} 模板。

反证:现有单测只有 tests/unit/test_user_review_fixes.py:681-686 覆盖 pow 上限,没有任何
mul / extend / 容器拼接的规模断言:

    def test_pow_exponent_capped(self):
        """$F.pow 指数上限:20 字节表达式不再能烧分钟级 CPU。"""
        from plaita.core.expression import _fn_pow
        with self.assertRaises(ValueError):
            _fn_pow(9, 9999999)
        self.assertEqual(_fn_pow(2, 10), 1024)

影响

  • 谁踩到:任何不被完全信任的流程作者或 IR 生成方。plaita 的定位是 AI/LLM 生成流程并发布
    执行(console 发布、MCP 生成 IR、@flow 源码),表达式与赋值节点是无处不在的作者可控输入;
    worker 是长驻进程,一次求值就吃掉 worker 的可用内存。
  • 条件与代价:内存放大在表达式求值内部发生,早于任何 I/O 与任何可中断点。因此 flow 级
    timeout、HTTP 节点响应体上限、以及沙箱子进程的 RLIMIT_AS 都拦不到这条路径——被拖死的是
    worker/console 自身的 Python 进程。29 字节写 1GB、再加几个参数即可线性推到 2GB 以上;并行分支
    各自求值会叠加。代价是 OOM / RSS 膨胀到宿主无法回收,同 worker 上的其他执行一起受害。
  • 生产可达:常见形态即可达——流程作者在 assignment/节点参数里写 $F.mul('A', 1000000000),
    或直接写 big = "A" * 1000000000 并被编译成同一条表达式;无需特殊部署档位。
  • 不修的后果:$F.pow 当年触发的「单流程拖死 worker」现在原样保留在同族算子上,安全评审修掉的
    只是一个实例而不是这一类风险。

建议

  • 修复方向:在 plaita/core/expression.py 里把 pow 的守卫泛化成一族规模守卫,与 _fn_pow
    同形(同样的 ValueError 文案风格,便于作者定位):
    • _fn_mul(字符串/列表重复分支):在调用前探测 len(a) * b(b 为 int 时)或
      len(a) * len(b) 的有效结果规模;超过阈值直接 raise ValueError。
    • _fn_add / _fn_extend / _fn_append / _fn_join:对序列/字符串参数探测 len(a) + len(b)
      (或 join 的 Σ len)是否越界。
    • 阈值可以复用 _POW_MAX_EXPONENT 的思路抽出模块级常量(例如元素数/字符数上限),
      使「改一处即可调参」,避免散落魔法数。
  • 最小修法(一处):在 _fn_mul 加与 _fn_pow 同形的守卫即可掐掉 'A' * 1e9 这一条最廉价的
    放大路径(29 字节 → 1GB);容器侧 extend / join 紧随其后补齐,不必一次性重构求值器。
  • 验收条件(可执行、可断言):
    • 新增单测(可放在 tests/unit/test_user_review_fixes.py 与 test_pow_exponent_capped 相邻):
      断言 _fn_mul("A", 1000000000)、_fn_mul([0], 100000000)、_fn_extend([0] * 10, [0] * 100000000)
      均抛 ValueError;并保留正向用例断言小规模结果不变
      (_fn_mul("ab", 3) == "ababab"、_fn_extend([1], [2]) == [1, 2])。
    • 复现脚本应变为:p.evaluate("{% $F.mul('A', 1000000000) %}", ctx) 抛 ValueError
      (文案含上限值),而不是返回长度 10 亿的字符串;rss MB 不再出现 GB 级跳变。
    • 回归:$F.pow 现有守卫与 test_pow_exponent_capped 必须继续通过;$F.mul 的数值用例
      (如 tests/unit/test_codeflow_source.py:47、tests/unit/test_dsl.py:102 使用的
      F.mul(INPUT.item, 2) 形状)不得被新守卫误伤。

边界

  • 本单只处理表达式引擎内建算子的无界资源放大(mul 及其同族的字符串/列表多态,以及
    add / extend / append / join 的拼接放大)。不改 $F.div / $F.mod 的零除语义,
    不改表达式的解析/编译架构,也不引入通用配额系统。
  • 与相邻 issue 的差异(查重结论):gh issue list -R jeffkit/plaita --state all --limit 100
    共 33 条,#31(HTTP 节点响应体无大小上限)与 #41(已关闭:沙箱 RLIMIT_AS 在 Linux 强制生效)
    都不覆盖表达式求值器内部的多态放大——#31 是节点 I/O 层、#41 是子进程层,本单的分配发生在
    worker 自身进程的 Python 字节码里,三者的修复面互不重叠,故非重复。
  • 不顺手扩大范围:不要求同时修 $F.pow 已修部分的文案或阈值;若后续要做「求值器统一资源预算」
    (如按返回值规模统一拦截),那属于更大的设计变更,应另开单,本单只要求把已有守卫推广到同族算子。
Dominant language
Python
Stars
0
Forks
1
Avg merge
3h
Merged PRs (30d)
6

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.