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.
Files changed (66) hide show
  1. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/PKG-INFO +1 -1
  2. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/__version__.py +1 -1
  3. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/config.py +14 -0
  4. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/engine.py +12 -2
  5. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/interfaces.py +10 -0
  6. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/template/template.py +19 -0
  7. bamboo_engine-2.6.4/bamboo_engine/utils/mako_safety.py +379 -0
  8. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/checker.py +17 -5
  9. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/pyproject.toml +1 -1
  10. bamboo_engine-2.6.2/bamboo_engine/utils/mako_safety.py +0 -55
  11. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/LICENSE +0 -0
  12. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/__init__.py +0 -0
  13. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/api.py +0 -0
  14. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/__init__.py +0 -0
  15. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/builder.py +0 -0
  16. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/__init__.py +0 -0
  17. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/activity.py +0 -0
  18. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/base.py +0 -0
  19. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/data.py +0 -0
  20. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/event.py +0 -0
  21. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/builder/flow/gateway.py +0 -0
  22. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/context.py +0 -0
  23. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/__init__.py +0 -0
  24. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/__init__.py +0 -0
  25. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/event.py +0 -0
  26. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/handler.py +0 -0
  27. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/interrupt.py +0 -0
  28. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/node.py +0 -0
  29. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/eri/models/runtime.py +0 -0
  30. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/exceptions.py +0 -0
  31. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handler.py +0 -0
  32. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/__init__.py +0 -0
  33. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/conditional_parallel_gateway.py +0 -0
  34. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/converge_gateway.py +0 -0
  35. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/empty_end_event.py +0 -0
  36. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/empty_start_event.py +0 -0
  37. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/exclusive_gateway.py +0 -0
  38. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/executable_end_event.py +0 -0
  39. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/parallel_gateway.py +0 -0
  40. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/service_activity.py +0 -0
  41. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/handlers/subprocess.py +0 -0
  42. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/interrupt.py +0 -0
  43. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/local.py +0 -0
  44. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/metrics.py +0 -0
  45. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/states.py +0 -0
  46. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/template/__init__.py +0 -0
  47. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/template/sandbox.py +0 -0
  48. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/__init__.py +0 -0
  49. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/boolrule/__init__.py +0 -0
  50. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/boolrule/boolrule.py +0 -0
  51. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/collections.py +0 -0
  52. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/constants.py +0 -0
  53. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/graph.py +0 -0
  54. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/host.py +0 -0
  55. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/__init__.py +0 -0
  56. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/code_extract.py +0 -0
  57. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/exceptions.py +0 -0
  58. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/mako_utils/visitors.py +0 -0
  59. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/object.py +0 -0
  60. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/utils/string.py +0 -0
  61. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/__init__.py +0 -0
  62. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/api.py +0 -0
  63. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/connection.py +0 -0
  64. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/gateway.py +0 -0
  65. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/rules.py +0 -0
  66. {bamboo_engine-2.6.2 → bamboo_engine-2.6.4}/bamboo_engine/validator/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bamboo-engine
3
- Version: 2.6.2
3
+ Version: 2.6.4
4
4
  Summary: Bamboo-engine is a general-purpose workflow engine
5
5
  License: MIT
6
6
  Author: blueking
@@ -11,4 +11,4 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express o
11
11
  specific language governing permissions and limitations under the License.
12
12
  """
13
13
 
14
- __version__ = "2.6.2"
14
+ __version__ = "2.6.4"
@@ -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
- retry_headers[self.CALLBACK_LOCK_RETRY_HEADER] = self._schedule_lock_retry_count(headers) + 1
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
- continue
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "bamboo-engine"
3
- version = "2.6.2"
3
+ version = "2.6.4"
4
4
  description = "Bamboo-engine is a general-purpose workflow engine"
5
5
  authors = ["blueking <blueking@tencent.com>"]
6
6
  license = "MIT"
@@ -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