bamboo-engine 2.6.2__tar.gz → 2.6.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/PKG-INFO +1 -1
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/__version__.py +1 -1
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/config.py +14 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/engine.py +12 -2
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/interfaces.py +10 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/template/template.py +19 -0
- bamboo_engine-2.6.4/bamboo_engine/utils/mako_safety.py +379 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/checker.py +17 -5
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/pyproject.toml +1 -1
- bamboo_engine-2.6.2/bamboo_engine/utils/mako_safety.py +0 -55
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/LICENSE +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/api.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/builder.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/activity.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/base.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/data.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/event.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/gateway.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/context.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/event.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/handler.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/interrupt.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/node.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/runtime.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/exceptions.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handler.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/conditional_parallel_gateway.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/converge_gateway.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/empty_end_event.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/empty_start_event.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/exclusive_gateway.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/executable_end_event.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/parallel_gateway.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/service_activity.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/subprocess.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/interrupt.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/local.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/metrics.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/states.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/template/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/template/sandbox.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/boolrule/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/boolrule/boolrule.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/collections.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/constants.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/graph.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/host.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/code_extract.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/exceptions.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/visitors.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/object.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/string.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/__init__.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/api.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/connection.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/gateway.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/rules.py +0 -0
- {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/utils.py +0 -0
|
@@ -21,6 +21,7 @@ class Settings:
|
|
|
21
21
|
|
|
22
22
|
MAKO_SANDBOX_SHIELD_WORDS = [
|
|
23
23
|
"ascii",
|
|
24
|
+
"breakpoint",
|
|
24
25
|
"bytearray",
|
|
25
26
|
"bytes",
|
|
26
27
|
"callable",
|
|
@@ -33,6 +34,7 @@ class Settings:
|
|
|
33
34
|
"exec",
|
|
34
35
|
"eval",
|
|
35
36
|
"filter",
|
|
37
|
+
"format",
|
|
36
38
|
"frozenset",
|
|
37
39
|
"getattr",
|
|
38
40
|
"globals",
|
|
@@ -63,4 +65,16 @@ class Settings:
|
|
|
63
65
|
|
|
64
66
|
MAKO_SANDBOX_IMPORT_MODULES = {}
|
|
65
67
|
|
|
68
|
+
# 模板根标识符白名单模式:
|
|
69
|
+
# "off" - 关闭白名单(兼容历史行为)
|
|
70
|
+
# "warn" - 仅记录违规日志,不阻断渲染(灰度阶段使用)
|
|
71
|
+
# "enforce" - 违规即按当前 deny-list 风格拦截(行为同 ForbiddenMakoTemplateException)
|
|
72
|
+
# 默认 "enforce",避免默认安装后继续暴露 Mako 渲染期保留命名空间注入链路。
|
|
73
|
+
# 如接入方确有兼容性风险,可在自身 settings 中临时切到 "warn" 或 "off"。
|
|
74
|
+
MAKO_TEMPLATE_NAME_WHITELIST_MODE = "enforce"
|
|
75
|
+
|
|
76
|
+
# 在白名单基础上额外允许的根标识符(除 context/导入模块/SAFE_BUILTIN_NAMES 之外)。
|
|
77
|
+
# 接入方通常用于声明 ``_system``、``_loop`` 这类 Mako 渲染期才注入的特殊对象名。
|
|
78
|
+
MAKO_TEMPLATE_NAME_EXTRA_WHITELIST = frozenset()
|
|
79
|
+
|
|
66
80
|
RERUN_INDEX_OFFSET = 0
|
|
@@ -80,6 +80,7 @@ class Engine:
|
|
|
80
80
|
PURE_SKIP_ENABLE_NODE_TYPE = {NodeType.ServiceActivity, NodeType.EmptyStartEvent}
|
|
81
81
|
CALLBACK_LOCK_RETRY_HEADER = "callback_lock_retry_times"
|
|
82
82
|
CALLBACK_LOCK_RETRY_LIMIT = 3
|
|
83
|
+
CALLBACK_LOCK_RETRY_DELAY_RANGES = ((1, 3), (3, 6), (6, 10))
|
|
83
84
|
|
|
84
85
|
def __init__(self, runtime: EngineRuntimeInterface):
|
|
85
86
|
self.runtime = runtime
|
|
@@ -103,6 +104,12 @@ class Engine:
|
|
|
103
104
|
service = self.runtime.get_service(code=node.code, version=node.version)
|
|
104
105
|
return callback_data, service.callback_lock_retryable(callback_data=callback_data.data)
|
|
105
106
|
|
|
107
|
+
def _callback_lock_retry_delay(self, retry_count: int) -> int:
|
|
108
|
+
delay_range = self.CALLBACK_LOCK_RETRY_DELAY_RANGES[
|
|
109
|
+
min(retry_count, len(self.CALLBACK_LOCK_RETRY_DELAY_RANGES) - 1)
|
|
110
|
+
]
|
|
111
|
+
return random.randint(*delay_range)
|
|
112
|
+
|
|
106
113
|
# api
|
|
107
114
|
def run_pipeline(
|
|
108
115
|
self,
|
|
@@ -1045,10 +1052,13 @@ class Engine:
|
|
|
1045
1052
|
)
|
|
1046
1053
|
return
|
|
1047
1054
|
|
|
1048
|
-
try_after = random.randint(1, 5)
|
|
1049
1055
|
retry_headers = dict(headers or {})
|
|
1050
1056
|
if schedule.type is ScheduleType.CALLBACK:
|
|
1051
|
-
|
|
1057
|
+
current_retry_count = self._schedule_lock_retry_count(headers)
|
|
1058
|
+
try_after = self._callback_lock_retry_delay(current_retry_count)
|
|
1059
|
+
retry_headers[self.CALLBACK_LOCK_RETRY_HEADER] = current_retry_count + 1
|
|
1060
|
+
else:
|
|
1061
|
+
try_after = random.randint(1, 5)
|
|
1052
1062
|
logger.info(
|
|
1053
1063
|
"root pipeline[%s] schedule(%s) lock %s with data %s fetch fail, try after %s, "
|
|
1054
1064
|
"retry_count=%s, callback_data=%s",
|
|
@@ -113,6 +113,16 @@ class Service(metaclass=ABCMeta):
|
|
|
113
113
|
:rtype: bool
|
|
114
114
|
"""
|
|
115
115
|
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def callback_lock_retryable(self, callback_data=None) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
回调调度锁冲突时是否允许重试
|
|
120
|
+
|
|
121
|
+
:param callback_data: 回调数据
|
|
122
|
+
:return: 是否允许重试
|
|
123
|
+
:rtype: bool
|
|
124
|
+
"""
|
|
125
|
+
|
|
116
126
|
@abstractmethod
|
|
117
127
|
def schedule_after(
|
|
118
128
|
self,
|
|
@@ -23,6 +23,7 @@ from mako.template import Template as MakoTemplate
|
|
|
23
23
|
from mako import lexer, codegen
|
|
24
24
|
from mako.exceptions import MakoException
|
|
25
25
|
|
|
26
|
+
from bamboo_engine.config import Settings
|
|
26
27
|
from bamboo_engine.utils.mako_utils.checker import check_mako_template_safety
|
|
27
28
|
from bamboo_engine.utils.mako_utils.exceptions import ForbiddenMakoTemplateException
|
|
28
29
|
from bamboo_engine.utils import mako_safety
|
|
@@ -159,6 +160,24 @@ class Template:
|
|
|
159
160
|
except Exception:
|
|
160
161
|
logger.exception("{} safety check error.".format(tpl))
|
|
161
162
|
continue
|
|
163
|
+
# 根标识符白名单(``MAKO_TEMPLATE_NAME_WHITELIST_MODE``)。off 模式下保持
|
|
164
|
+
# 历史行为;warn 模式只打日志;enforce 模式命中即按 SingleLineNodeVisitor
|
|
165
|
+
# 风格 inert 掉这一段模板(``logger.warning + continue``)。
|
|
166
|
+
whitelist_mode = getattr(Settings, "MAKO_TEMPLATE_NAME_WHITELIST_MODE", "off")
|
|
167
|
+
if whitelist_mode in ("warn", "enforce"):
|
|
168
|
+
allowed_names = mako_safety.build_allowed_names(context)
|
|
169
|
+
try:
|
|
170
|
+
check_mako_template_safety(
|
|
171
|
+
tpl,
|
|
172
|
+
mako_safety.WhitelistNameVisitor(allowed_names, mode=whitelist_mode),
|
|
173
|
+
mako_safety.SingleLinCodeExtractor(),
|
|
174
|
+
)
|
|
175
|
+
except ForbiddenMakoTemplateException as e:
|
|
176
|
+
logger.warning("forbidden by whitelist: {}, exception: {}".format(tpl, e))
|
|
177
|
+
continue
|
|
178
|
+
except Exception:
|
|
179
|
+
logger.exception("{} whitelist check error.".format(tpl))
|
|
180
|
+
continue
|
|
162
181
|
resolved = Template._render_template(tpl, context)
|
|
163
182
|
string = string.replace(tpl, resolved)
|
|
164
183
|
return string
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
|
4
|
+
Edition) available.
|
|
5
|
+
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
|
|
6
|
+
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
http://opensource.org/licenses/MIT
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
|
10
|
+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
11
|
+
specific language governing permissions and limitations under the License.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Mako 安全工具
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from mako import parsetree
|
|
22
|
+
|
|
23
|
+
from .mako_utils.code_extract import MakoNodeCodeExtractor
|
|
24
|
+
from .mako_utils.exceptions import ForbiddenMakoTemplateException
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("root")
|
|
28
|
+
|
|
29
|
+
# 与 ``MAKO_SANDBOX_SHIELD_WORDS`` 不重叠的"安全内建函数"集合。
|
|
30
|
+
# 用于白名单模式下默认放行的根标识符。
|
|
31
|
+
# 不包含 ``bytes/bytearray/frozenset/memoryview/object/type/vars/getattr/...``
|
|
32
|
+
# 等常出现在 shield 列表中的内建——它们即便能通过 AST 也会在渲染期被屏蔽成 ``None``,
|
|
33
|
+
# 留在白名单里只会带来认知噪音。
|
|
34
|
+
SAFE_BUILTIN_NAMES = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"True",
|
|
37
|
+
"False",
|
|
38
|
+
"None",
|
|
39
|
+
# 类型构造(不会构造危险对象)
|
|
40
|
+
"bool",
|
|
41
|
+
"int",
|
|
42
|
+
"float",
|
|
43
|
+
"str",
|
|
44
|
+
"list",
|
|
45
|
+
"tuple",
|
|
46
|
+
"dict",
|
|
47
|
+
"set",
|
|
48
|
+
# 数学 / 长度
|
|
49
|
+
"abs",
|
|
50
|
+
"round",
|
|
51
|
+
"pow",
|
|
52
|
+
"sum",
|
|
53
|
+
"min",
|
|
54
|
+
"max",
|
|
55
|
+
"len",
|
|
56
|
+
# 序列
|
|
57
|
+
"range",
|
|
58
|
+
"slice",
|
|
59
|
+
"enumerate",
|
|
60
|
+
"zip",
|
|
61
|
+
"sorted",
|
|
62
|
+
"reversed",
|
|
63
|
+
# 逻辑
|
|
64
|
+
"all",
|
|
65
|
+
"any",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Mako 在渲染期会向模板命名空间注入的保留对象名,用户模板里出现这些名字时
|
|
70
|
+
# 极大概率是在尝试触达模板内部对象(``self.module.cache.util.os...`` SSTI 链路)。
|
|
71
|
+
# 对它们直接拒绝,可以堵住绝大多数 namespace 链式 RCE 路径。
|
|
72
|
+
MAKO_RESERVED_NAMESPACES = frozenset(
|
|
73
|
+
{
|
|
74
|
+
"self",
|
|
75
|
+
"context",
|
|
76
|
+
"local",
|
|
77
|
+
"parent",
|
|
78
|
+
"next",
|
|
79
|
+
"caller",
|
|
80
|
+
"pageargs",
|
|
81
|
+
"UNDEFINED",
|
|
82
|
+
"STOP_RENDERING",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# attr 链路上一旦出现下列名字就立刻拒绝。
|
|
87
|
+
#
|
|
88
|
+
# 根 ``Name`` 白名单只能防"未授权根标识符",挡不住通过白名单根名(如 ``os``、``datetime``、
|
|
89
|
+
# ``re``)反向触达危险模块的链路:
|
|
90
|
+
#
|
|
91
|
+
# * ``${os.path.os.popen(...)}`` — ``os.path`` 内部 ``import os``
|
|
92
|
+
# * ``${datetime.sys.modules['os']...}`` — ``datetime`` 内部 ``import sys``
|
|
93
|
+
# * ``${re.enum.sys.modules['os']...}`` — ``re`` 重新 export ``enum``,``enum`` 内 ``import sys``
|
|
94
|
+
#
|
|
95
|
+
# 所有这些链路都需要 attr 链上出现 ``os / sys / subprocess / shutil / ctypes / socket /
|
|
96
|
+
# builtins / modules`` 之一,或调用 ``popen / system / exec* / spawn* / fork*`` 之一。
|
|
97
|
+
# 直接把 attr 名拉黑,就能切掉公共必经路径。这是纵深防御里的"任何路径都不该带这些字",
|
|
98
|
+
# 不是替代根名白名单。
|
|
99
|
+
DANGEROUS_ATTR_NAMES = frozenset(
|
|
100
|
+
{
|
|
101
|
+
# 直接拿到危险模块对象
|
|
102
|
+
"os",
|
|
103
|
+
"sys",
|
|
104
|
+
"subprocess",
|
|
105
|
+
"shutil",
|
|
106
|
+
"ctypes",
|
|
107
|
+
"socket",
|
|
108
|
+
"_thread",
|
|
109
|
+
"threading",
|
|
110
|
+
"builtins",
|
|
111
|
+
"__builtins__",
|
|
112
|
+
# 通过模块字典反向触达任意已导入模块
|
|
113
|
+
"modules",
|
|
114
|
+
# 进程 / 文件描述符相关原语(即便有人未来误把 ``os`` 加进白名单,也再加一层)
|
|
115
|
+
"popen",
|
|
116
|
+
"popen2",
|
|
117
|
+
"popen3",
|
|
118
|
+
"popen4",
|
|
119
|
+
"system",
|
|
120
|
+
"spawnl",
|
|
121
|
+
"spawnle",
|
|
122
|
+
"spawnlp",
|
|
123
|
+
"spawnlpe",
|
|
124
|
+
"spawnv",
|
|
125
|
+
"spawnve",
|
|
126
|
+
"spawnvp",
|
|
127
|
+
"spawnvpe",
|
|
128
|
+
"execl",
|
|
129
|
+
"execle",
|
|
130
|
+
"execlp",
|
|
131
|
+
"execlpe",
|
|
132
|
+
"execv",
|
|
133
|
+
"execve",
|
|
134
|
+
"execvp",
|
|
135
|
+
"execvpe",
|
|
136
|
+
"fork",
|
|
137
|
+
"forkpty",
|
|
138
|
+
"kill",
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
FORBIDDEN_TEMPLATE_METHODS = {"format", "format_map"}
|
|
143
|
+
SAFE_FILTERS = {"n", "h", "x", "u", "trim", "entity", "unicode", "str"}
|
|
144
|
+
SAFE_DECODE_FILTER_PATTERN = re.compile(r"^decode\.[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SingleLineNodeVisitor(ast.NodeVisitor):
|
|
148
|
+
"""
|
|
149
|
+
遍历语法树节点,遇到魔术方法使用或 import 时,抛出异常
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, *args, **kwargs):
|
|
153
|
+
super(SingleLineNodeVisitor, self).__init__(*args, **kwargs)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _get_subscript_key(node):
|
|
157
|
+
slice_node = node.slice
|
|
158
|
+
constant_node = getattr(ast, "Constant", None)
|
|
159
|
+
if (
|
|
160
|
+
constant_node is not None
|
|
161
|
+
and isinstance(slice_node, constant_node)
|
|
162
|
+
and isinstance(slice_node.value, str)
|
|
163
|
+
):
|
|
164
|
+
return slice_node.value
|
|
165
|
+
if hasattr(ast, "Str") and isinstance(slice_node, ast.Str):
|
|
166
|
+
return slice_node.s
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def visit_Attribute(self, node):
|
|
170
|
+
if node.attr.startswith("__"):
|
|
171
|
+
raise ForbiddenMakoTemplateException("can not access private attribute")
|
|
172
|
+
if node.attr in FORBIDDEN_TEMPLATE_METHODS:
|
|
173
|
+
raise ForbiddenMakoTemplateException("can not call forbidden method")
|
|
174
|
+
self.generic_visit(node)
|
|
175
|
+
|
|
176
|
+
def visit_Name(self, node):
|
|
177
|
+
if node.id.startswith("__"):
|
|
178
|
+
raise ForbiddenMakoTemplateException("can not access private method")
|
|
179
|
+
self.generic_visit(node)
|
|
180
|
+
|
|
181
|
+
def visit_Subscript(self, node):
|
|
182
|
+
subscript_key = self._get_subscript_key(node)
|
|
183
|
+
if isinstance(subscript_key, str) and subscript_key.startswith("__"):
|
|
184
|
+
raise ForbiddenMakoTemplateException("can not access private key")
|
|
185
|
+
self.generic_visit(node)
|
|
186
|
+
|
|
187
|
+
def visit_Call(self, node):
|
|
188
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr in FORBIDDEN_TEMPLATE_METHODS:
|
|
189
|
+
raise ForbiddenMakoTemplateException("can not call forbidden method")
|
|
190
|
+
self.generic_visit(node)
|
|
191
|
+
|
|
192
|
+
def visit_Import(self, node):
|
|
193
|
+
raise ForbiddenMakoTemplateException("can not use import statement")
|
|
194
|
+
|
|
195
|
+
def visit_ImportFrom(self, node):
|
|
196
|
+
self.visit_Import(node)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate_filter_args(filter_args):
|
|
200
|
+
for filter_arg in filter_args:
|
|
201
|
+
normalized_filter = filter_arg.strip()
|
|
202
|
+
if normalized_filter in SAFE_FILTERS:
|
|
203
|
+
continue
|
|
204
|
+
decode_filter_parts = normalized_filter.split(".")
|
|
205
|
+
if (
|
|
206
|
+
SAFE_DECODE_FILTER_PATTERN.match(normalized_filter)
|
|
207
|
+
and "__" not in normalized_filter
|
|
208
|
+
and not any(part.startswith("_") for part in decode_filter_parts[1:])
|
|
209
|
+
):
|
|
210
|
+
continue
|
|
211
|
+
raise ForbiddenMakoTemplateException("unsupported filter expression: [{}]".format(normalized_filter))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class SingleLinCodeExtractor(MakoNodeCodeExtractor):
|
|
215
|
+
def extract(self, node):
|
|
216
|
+
if isinstance(node, parsetree.Code) or isinstance(node, parsetree.Expression):
|
|
217
|
+
return node.text
|
|
218
|
+
elif isinstance(node, parsetree.Text):
|
|
219
|
+
return None
|
|
220
|
+
else:
|
|
221
|
+
raise ForbiddenMakoTemplateException("Unsupported node: [{}]".format(node.__class__.__name__))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _deformat_var_key(key):
|
|
225
|
+
"""``${name}`` -> ``name``;其它 key 原样返回。"""
|
|
226
|
+
if isinstance(key, str) and key.startswith("${") and key.endswith("}"):
|
|
227
|
+
return key[2:-1]
|
|
228
|
+
return key
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def build_allowed_names(context, *, extra=()):
|
|
232
|
+
"""根据当前渲染 context 与全局 ``Settings`` 计算白名单的根标识符集合。
|
|
233
|
+
|
|
234
|
+
包含:
|
|
235
|
+
* ``context`` 中的键(``${name}`` 形式自动 deformat)
|
|
236
|
+
* ``Settings.MAKO_SANDBOX_IMPORT_MODULES`` 中每个 alias 的首段
|
|
237
|
+
(例:``os.path`` → ``os``)
|
|
238
|
+
* :data:`SAFE_BUILTIN_NAMES`
|
|
239
|
+
* ``Settings.MAKO_TEMPLATE_NAME_EXTRA_WHITELIST``
|
|
240
|
+
* 调用方传入的 ``extra``
|
|
241
|
+
"""
|
|
242
|
+
# 局部 import 避免循环依赖(``Settings`` 在 ``bamboo_engine.config``)。
|
|
243
|
+
from bamboo_engine.config import Settings
|
|
244
|
+
|
|
245
|
+
allowed = set(SAFE_BUILTIN_NAMES)
|
|
246
|
+
|
|
247
|
+
for key in context.keys() if context else ():
|
|
248
|
+
allowed.add(_deformat_var_key(key))
|
|
249
|
+
|
|
250
|
+
import_modules = getattr(Settings, "MAKO_SANDBOX_IMPORT_MODULES", None) or {}
|
|
251
|
+
for alias in import_modules.values():
|
|
252
|
+
if alias:
|
|
253
|
+
allowed.add(alias.split(".", 1)[0])
|
|
254
|
+
|
|
255
|
+
extra_whitelist = getattr(Settings, "MAKO_TEMPLATE_NAME_EXTRA_WHITELIST", None) or ()
|
|
256
|
+
allowed.update(extra_whitelist)
|
|
257
|
+
allowed.update(extra)
|
|
258
|
+
|
|
259
|
+
return allowed
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class WhitelistNameVisitor(ast.NodeVisitor):
|
|
263
|
+
"""根标识符白名单 visitor。
|
|
264
|
+
|
|
265
|
+
只允许 Load 语义的 ``Name`` 节点引用 ``allowed_names`` 中的标识符;其余一律按
|
|
266
|
+
``mode`` 处理:
|
|
267
|
+
|
|
268
|
+
* ``warn``:调用 ``on_violation`` / 打 warning 日志,**不抛异常**(灰度模式)。
|
|
269
|
+
* ``enforce``:抛 :exc:`ForbiddenMakoTemplateException`,由
|
|
270
|
+
:func:`bamboo_engine.utils.mako_utils.checker.check_mako_template_safety` 捕获。
|
|
271
|
+
|
|
272
|
+
本 visitor 还显式拦截 :data:`MAKO_RESERVED_NAMESPACES` 中的标识符,
|
|
273
|
+
无论是否被传入 ``allowed_names`` 都会被拒,避免误把 ``self/context/...`` 加进
|
|
274
|
+
上下文导致 SSTI 链路被放行。
|
|
275
|
+
|
|
276
|
+
支持 ``ListComp / SetComp / DictComp / GeneratorExp / Lambda`` 引入的局部
|
|
277
|
+
绑定——这些临时变量会被压入作用域栈,在子树访问完后自动弹出。
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
def __init__(self, allowed_names, mode="enforce", on_violation=None):
|
|
281
|
+
if mode not in {"warn", "enforce"}:
|
|
282
|
+
raise ValueError("invalid whitelist mode: {}".format(mode))
|
|
283
|
+
self.allowed_names = set(allowed_names)
|
|
284
|
+
self.mode = mode
|
|
285
|
+
self.on_violation = on_violation
|
|
286
|
+
self.scope_stack = []
|
|
287
|
+
|
|
288
|
+
def _name_allowed(self, name):
|
|
289
|
+
if name in MAKO_RESERVED_NAMESPACES:
|
|
290
|
+
return False
|
|
291
|
+
if name in self.allowed_names:
|
|
292
|
+
return True
|
|
293
|
+
for scope in self.scope_stack:
|
|
294
|
+
if name in scope:
|
|
295
|
+
return True
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
def _violate(self, name, reason):
|
|
299
|
+
msg = "name not in whitelist: {} ({})".format(name, reason)
|
|
300
|
+
if self.on_violation is not None:
|
|
301
|
+
try:
|
|
302
|
+
self.on_violation(name, reason)
|
|
303
|
+
except Exception: # pragma: no cover - defensive
|
|
304
|
+
logger.exception("on_violation callback raised")
|
|
305
|
+
if self.mode == "enforce":
|
|
306
|
+
raise ForbiddenMakoTemplateException(msg)
|
|
307
|
+
logger.warning("[mako_whitelist] %s", msg)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _collect_targets(target, into):
|
|
311
|
+
if isinstance(target, ast.Name):
|
|
312
|
+
into.add(target.id)
|
|
313
|
+
elif isinstance(target, ast.Starred):
|
|
314
|
+
WhitelistNameVisitor._collect_targets(target.value, into)
|
|
315
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
316
|
+
for elt in target.elts:
|
|
317
|
+
WhitelistNameVisitor._collect_targets(elt, into)
|
|
318
|
+
|
|
319
|
+
def visit_Name(self, node):
|
|
320
|
+
# Store / Del 上下文是赋值/删除目标,由 _enter_* 显式处理,跳过命名检查
|
|
321
|
+
if not isinstance(node.ctx, ast.Load):
|
|
322
|
+
return
|
|
323
|
+
if node.id in MAKO_RESERVED_NAMESPACES:
|
|
324
|
+
self._violate(node.id, "mako reserved namespace")
|
|
325
|
+
return
|
|
326
|
+
if not self._name_allowed(node.id):
|
|
327
|
+
self._violate(node.id, "not in whitelist")
|
|
328
|
+
|
|
329
|
+
def visit_Attribute(self, node):
|
|
330
|
+
# 单下划线 attr 拒绝:堵 ``context._with_template / context._kwargs`` 这类
|
|
331
|
+
# Python "半私有" 通道(双下划线已经在 ``SingleLineNodeVisitor`` 拦过,这里
|
|
332
|
+
# 顺手收紧成单下划线,覆盖更广)。
|
|
333
|
+
if node.attr.startswith("_"):
|
|
334
|
+
self._violate(node.attr, "private attribute")
|
|
335
|
+
return
|
|
336
|
+
# 危险 attr 名拒绝:堵 ``${os.path.os.popen(...)}`` /
|
|
337
|
+
# ``${datetime.sys.modules['os'].popen(...)}`` 类反向引用链路。
|
|
338
|
+
if node.attr in DANGEROUS_ATTR_NAMES:
|
|
339
|
+
self._violate(node.attr, "dangerous attribute")
|
|
340
|
+
return
|
|
341
|
+
self.generic_visit(node)
|
|
342
|
+
|
|
343
|
+
def _enter_comprehension(self, node):
|
|
344
|
+
local = set()
|
|
345
|
+
for gen in node.generators:
|
|
346
|
+
self._collect_targets(gen.target, local)
|
|
347
|
+
self.scope_stack.append(local)
|
|
348
|
+
try:
|
|
349
|
+
self.generic_visit(node)
|
|
350
|
+
finally:
|
|
351
|
+
self.scope_stack.pop()
|
|
352
|
+
|
|
353
|
+
def visit_ListComp(self, node):
|
|
354
|
+
self._enter_comprehension(node)
|
|
355
|
+
|
|
356
|
+
def visit_SetComp(self, node):
|
|
357
|
+
self._enter_comprehension(node)
|
|
358
|
+
|
|
359
|
+
def visit_DictComp(self, node):
|
|
360
|
+
self._enter_comprehension(node)
|
|
361
|
+
|
|
362
|
+
def visit_GeneratorExp(self, node):
|
|
363
|
+
self._enter_comprehension(node)
|
|
364
|
+
|
|
365
|
+
def visit_Lambda(self, node):
|
|
366
|
+
local = set()
|
|
367
|
+
args = node.args
|
|
368
|
+
local.update(arg.arg for arg in args.args)
|
|
369
|
+
local.update(arg.arg for arg in args.kwonlyargs)
|
|
370
|
+
local.update(arg.arg for arg in getattr(args, "posonlyargs", ()) or ())
|
|
371
|
+
if args.vararg:
|
|
372
|
+
local.add(args.vararg.arg)
|
|
373
|
+
if args.kwarg:
|
|
374
|
+
local.add(args.kwarg.arg)
|
|
375
|
+
self.scope_stack.append(local)
|
|
376
|
+
try:
|
|
377
|
+
self.generic_visit(node)
|
|
378
|
+
finally:
|
|
379
|
+
self.scope_stack.pop()
|
|
@@ -19,10 +19,21 @@ from mako import parsetree
|
|
|
19
19
|
from mako.exceptions import MakoException
|
|
20
20
|
from mako.lexer import Lexer
|
|
21
21
|
|
|
22
|
+
from bamboo_engine.utils import mako_safety
|
|
23
|
+
|
|
22
24
|
from .code_extract import MakoNodeCodeExtractor
|
|
23
25
|
from .exceptions import ForbiddenMakoTemplateException
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def validate_node_filter_callables(node: parsetree.Node):
|
|
29
|
+
# Mako stores inline `${expr | ...}` filter callables in `escapes_code`;
|
|
30
|
+
# tag-level filters use `filter_args`. Both are executed by create_filter_callable().
|
|
31
|
+
if hasattr(node, "escapes_code"):
|
|
32
|
+
mako_safety.validate_filter_args(node.escapes_code.args)
|
|
33
|
+
if hasattr(node, "filter_args"):
|
|
34
|
+
mako_safety.validate_filter_args(node.filter_args.args)
|
|
35
|
+
|
|
36
|
+
|
|
26
37
|
def parse_template_nodes(
|
|
27
38
|
nodes: List[parsetree.Node],
|
|
28
39
|
node_visitor: ast.NodeVisitor,
|
|
@@ -35,14 +46,15 @@ def parse_template_nodes(
|
|
|
35
46
|
:param code_extractor: Mako 词法节点处理器,用于提取 python 代码
|
|
36
47
|
"""
|
|
37
48
|
for node in nodes:
|
|
49
|
+
validate_node_filter_callables(node)
|
|
50
|
+
|
|
38
51
|
code = code_extractor.extract(node)
|
|
39
|
-
if code is None:
|
|
40
|
-
|
|
52
|
+
if code is not None:
|
|
53
|
+
ast_node = ast.parse(code, "<unknown>", "exec")
|
|
54
|
+
node_visitor.visit(ast_node)
|
|
41
55
|
|
|
42
|
-
ast_node = ast.parse(code, "<unknown>", "exec")
|
|
43
|
-
node_visitor.visit(ast_node)
|
|
44
56
|
if hasattr(node, "nodes"):
|
|
45
|
-
parse_template_nodes(node.nodes, node_visitor)
|
|
57
|
+
parse_template_nodes(node.nodes, node_visitor, code_extractor)
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
def check_mako_template_safety(text: str, node_visitor: ast.NodeVisitor, code_extractor: MakoNodeCodeExtractor) -> bool:
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community
|
|
4
|
-
Edition) available.
|
|
5
|
-
Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
|
|
6
|
-
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
|
|
7
|
-
You may obtain a copy of the License at
|
|
8
|
-
http://opensource.org/licenses/MIT
|
|
9
|
-
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
|
10
|
-
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
11
|
-
specific language governing permissions and limitations under the License.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
# Mako 安全工具
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
from ast import NodeVisitor
|
|
18
|
-
|
|
19
|
-
from mako import parsetree
|
|
20
|
-
|
|
21
|
-
from .mako_utils.code_extract import MakoNodeCodeExtractor
|
|
22
|
-
from .mako_utils.exceptions import ForbiddenMakoTemplateException
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SingleLineNodeVisitor(NodeVisitor):
|
|
26
|
-
"""
|
|
27
|
-
遍历语法树节点,遇到魔术方法使用或 import 时,抛出异常
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, *args, **kwargs):
|
|
31
|
-
super(SingleLineNodeVisitor, self).__init__(*args, **kwargs)
|
|
32
|
-
|
|
33
|
-
def visit_Attribute(self, node):
|
|
34
|
-
if node.attr.startswith("__"):
|
|
35
|
-
raise ForbiddenMakoTemplateException("can not access private attribute")
|
|
36
|
-
|
|
37
|
-
def visit_Name(self, node):
|
|
38
|
-
if node.id.startswith("__"):
|
|
39
|
-
raise ForbiddenMakoTemplateException("can not access private method")
|
|
40
|
-
|
|
41
|
-
def visit_Import(self, node):
|
|
42
|
-
raise ForbiddenMakoTemplateException("can not use import statement")
|
|
43
|
-
|
|
44
|
-
def visit_ImportFrom(self, node):
|
|
45
|
-
self.visit_Import(node)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class SingleLinCodeExtractor(MakoNodeCodeExtractor):
|
|
49
|
-
def extract(self, node):
|
|
50
|
-
if isinstance(node, parsetree.Code) or isinstance(node, parsetree.Expression):
|
|
51
|
-
return node.text
|
|
52
|
-
elif isinstance(node, parsetree.Text):
|
|
53
|
-
return None
|
|
54
|
-
else:
|
|
55
|
-
raise ForbiddenMakoTemplateException("Unsupported node: [{}]".format(node.__class__.__name__))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/conditional_parallel_gateway.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|