Skip to content

Commit 6ff3642

Browse files
committed
fix: 安全加固 (closed #2726)
1 parent 6166cd6 commit 6ff3642

4 files changed

Lines changed: 502 additions & 12 deletions

File tree

apps/backend/agent/solution_maker.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ def shell_quote(value: typing.Any) -> str:
4141
return shlex.quote(value)
4242

4343

44+
def quote_path(path: str, os_type: str) -> str:
45+
"""
46+
根据操作系统类型,正确处理路径参数的引号
47+
48+
- Windows: 使用双引号包裹(bat脚本的 %~2 会自动去除双引号)
49+
- Linux/Mac: 使用 shell_quote (shlex.quote) 处理
50+
51+
Args:
52+
path: 路径字符串
53+
os_type: 操作系统类型(constants.OsType.WINDOWS 或 other)
54+
55+
Returns:
56+
处理后的路径字符串
57+
"""
58+
if not path:
59+
return path
60+
61+
# Windows 批处理:使用双引号包裹,%~2 会自动去除
62+
if os_type == constants.OsType.WINDOWS:
63+
# 如果路径已经包含双引号,直接返回
64+
if path.startswith('"') and path.endswith('"'):
65+
return path
66+
# 如果路径包含空格,用双引号包裹
67+
if " " in path:
68+
return f'"{path}"'
69+
return path
70+
71+
# Linux/Mac: 使用 shell_quote 处理
72+
return shell_quote(path)
73+
74+
4475
def normalize_host_identity_for_shell(value: typing.Any, auth_type: str) -> str:
4576
value = "" if value is None else str(value)
4677
if auth_type == constants.AuthType.KEY:
@@ -280,8 +311,10 @@ def get_run_cmd_base_params(self) -> typing.List[str]:
280311
f"-i {shell_quote(self.host.bk_cloud_id)}",
281312
f"-I {shell_quote(self.host.inner_ip or self.host.inner_ipv6)}",
282313
# 安装/下载配置
283-
f"-T {shell_quote(self.dest_dir)}",
284-
f"-p {shell_quote(self.agent_config['setup_path'])}",
314+
# Windows 批处理使用 %~2 会自动去除双引号
315+
# Linux/Mac 需要使用 shell_quote (shlex.quote) 来处理包含空格的路径
316+
f"-T {quote_path(self.dest_dir, self.host.os_type)}",
317+
f"-p {quote_path(self.agent_config['setup_path'], self.host.os_type)}",
285318
f"-c {shell_quote(self.token)}",
286319
f"-s {shell_quote(self.pipeline_id)}",
287320
]

apps/node_man/serializers/ap.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,260 @@
2929
from env.constants import GseVersion
3030

3131

32+
def validate_agent_config(agent_config):
33+
"""
34+
校验 agent_config 字段的合法性和安全性
35+
36+
Args:
37+
agent_config: Agent配置字典,格式为 {os_type: {config_key: config_value}}
38+
39+
Returns:
40+
校验后的 agent_config
41+
42+
Raises:
43+
ValidationError: 当配置不合法时抛出异常
44+
"""
45+
# 定义允许的操作系统类型
46+
allowed_os_types = {"linux", "windows", "aix", "solaris", "darwin"}
47+
48+
# 定义每个操作系统下允许的字段及其类型
49+
allowed_fields = {
50+
"setup_path": str, # Agent 安装路径
51+
"temp_path": str, # 临时文件目录
52+
"log_path": str, # 日志路径
53+
"data_path": str, # 数据路径
54+
"pluginipc": (str, int), # 插件 IPC 配置
55+
"dataipc": (str, int), # 数据 IPC 配置
56+
"alarm_event_data_id": int, # 告警事件数据 ID
57+
}
58+
59+
# 危险路径关键字,禁止出现在路径中
60+
dangerous_patterns = [
61+
"..", # 路径遍历
62+
"~", # 家目录
63+
"$(", # 命令替换
64+
"`", # 命令替换
65+
"|", # 管道
66+
";", # 命令分隔符
67+
"&", # 后台执行(必须放在 && 之前)
68+
"&&", # 命令连接
69+
"||", # 命令连接
70+
">", # 重定向
71+
"<", # 重定向
72+
"\n", # 换行符
73+
"\r", # 回车符
74+
"$", # 变量引用
75+
"(", # 子shell
76+
")", # 子shell
77+
"{", # 代码块
78+
"}", # 代码块
79+
"[", # 通配符
80+
"]", # 通配符
81+
"!", # 历史扩展
82+
"\\", # 转义符
83+
]
84+
85+
if not isinstance(agent_config, dict):
86+
raise ValidationError(_("agent_config 必须是字典类型"))
87+
88+
for os_type, config in agent_config.items():
89+
# 校验操作系统类型
90+
if os_type.lower() not in allowed_os_types:
91+
raise ValidationError(_("不支持的操作系统类型: {os_type}").format(os_type=os_type))
92+
93+
if not isinstance(config, dict):
94+
raise ValidationError(_("操作系统 {os_type} 的配置必须是字典类型").format(os_type=os_type))
95+
96+
for key, value in config.items():
97+
# 校验字段名是否合法
98+
if key not in allowed_fields:
99+
raise ValidationError(
100+
_("操作系统 {os_type} 包含不支持的配置项: {key}").format(os_type=os_type, key=key)
101+
)
102+
103+
# 校验字段类型
104+
expected_type = allowed_fields[key]
105+
if not isinstance(value, expected_type):
106+
raise ValidationError(
107+
_("操作系统 {os_type} 的配置项 {key} 类型错误,期望 {expected_type},实际为 {actual_type}").format(
108+
os_type=os_type,
109+
key=key,
110+
expected_type=expected_type.__name__,
111+
actual_type=type(value).__name__,
112+
)
113+
)
114+
115+
# 如果是路径类型的字段,进行安全校验
116+
if key in ["setup_path", "temp_path", "log_path", "data_path", "pluginipc", "dataipc"]:
117+
if isinstance(value, str):
118+
# 检查危险字符
119+
for pattern in dangerous_patterns:
120+
if pattern in value:
121+
raise ValidationError(
122+
_("操作系统 {os_type} 的配置项 {key} 包含不安全的字符: {pattern}").format(
123+
os_type=os_type, key=key, pattern=pattern
124+
)
125+
)
126+
127+
# 检查是否以 / 或盘符开头(绝对路径)
128+
if os_type.lower() == "windows":
129+
# Windows 路径应该以盘符开头,如 C:\ 或 C:/
130+
if not (value[0].isalpha() and value[1:3] in [":\\", ":/"]):
131+
# 相对路径也不允许
132+
if not (value.startswith(".\\") or value.startswith("./")):
133+
raise ValidationError(
134+
_("操作系统 {os_type} 的配置项 {key} 必须是绝对路径").format(
135+
os_type=os_type, key=key
136+
)
137+
)
138+
else:
139+
# Linux/Unix 路径应该以 / 开头
140+
if not value.startswith("/"):
141+
raise ValidationError(
142+
_("操作系统 {os_type} 的配置项 {key} 必须是绝对路径").format(
143+
os_type=os_type, key=key
144+
)
145+
)
146+
147+
return agent_config
148+
149+
150+
def validate_port_config(port_config):
151+
"""
152+
校验 port_config 字段的合法性和安全性
153+
154+
Args:
155+
port_config: 端口配置字典
156+
157+
Returns:
158+
校验后的 port_config
159+
160+
Raises:
161+
ValidationError: 当配置不合法时抛出异常
162+
"""
163+
# 定义允许的端口配置字段
164+
allowed_fields = {
165+
"agent_port", # Agent 端口
166+
"bt_port", # BT 端口
167+
"data_port", # 数据端口
168+
"file_port", # 文件端口
169+
"info_port", # 信息端口
170+
"listen_port", # 监听端口
171+
"mesh_port", # Mesh 端口
172+
}
173+
174+
# 危险字符黑名单
175+
dangerous_patterns = [
176+
"$((", "$(`", "$([", "$(<", "$(>", # 命令替换变种
177+
"$(", "`", "|", ";", "&", "&&", "||", ">", "<", # 原黑名单
178+
"\n", "\r", "$", "(", ")", "{", "}", "[", "]", "!", "\\",
179+
]
180+
181+
if not isinstance(port_config, dict):
182+
raise ValidationError(_("port_config 必须是字典类型"))
183+
184+
for key, value in port_config.items():
185+
# 校验字段名是否合法
186+
if key not in allowed_fields:
187+
raise ValidationError(
188+
_("不支持的端口配置项: {key}").format(key=key)
189+
)
190+
191+
# 校验端口号必须是正整数
192+
if not isinstance(value, int):
193+
raise ValidationError(
194+
_("端口配置项 {key} 必须是正整数").format(key=key)
195+
)
196+
197+
# 校验端口号范围(1-65535)
198+
if not (1 <= value <= 65535):
199+
raise ValidationError(
200+
_("端口配置项 {key} 必须在 1-65535 范围内").format(key=key)
201+
)
202+
203+
# 如果值是字符串(不应该出现,但以防万一),检查危险字符
204+
if isinstance(value, str):
205+
for pattern in dangerous_patterns:
206+
if pattern in value:
207+
raise ValidationError(
208+
_("端口配置项 {key} 包含不安全的字符: {pattern}").format(
209+
key=key, pattern=pattern
210+
)
211+
)
212+
213+
return port_config
214+
215+
216+
def validate_bscp_config(bscp_config):
217+
"""
218+
校验 bscp_config 字段的合法性和安全性
219+
220+
Args:
221+
bscp_config: BSCP 配置字典
222+
223+
Returns:
224+
校验后的 bscp_config
225+
226+
Raises:
227+
ValidationError: 当配置不合法时抛出异常
228+
"""
229+
# 定义允许的 BSCP 配置字段
230+
allowed_fields = {
231+
"server_url": str, # BSCP 服务器地址
232+
"config_path": str, # 配置文件路径
233+
"log_path": str, # 日志路径
234+
"data_path": str, # 数据路径
235+
}
236+
237+
# 危险字符黑名单(复用 agent_config 的黑名单)
238+
dangerous_patterns = [
239+
"..", "~", "$(", "`", "|", ";", "&", "&&", "||",
240+
">", "<", "\n", "\r", "$", "(", ")", "{", "}",
241+
"[", "]", "!", "\\",
242+
]
243+
244+
if not isinstance(bscp_config, dict):
245+
raise ValidationError(_("bscp_config 必须是字典类型"))
246+
247+
for key, value in bscp_config.items():
248+
# 校验字段名是否合法
249+
if key not in allowed_fields:
250+
raise ValidationError(
251+
_("不支持的 BSCP 配置项: {key}").format(key=key)
252+
)
253+
254+
# 校验字段类型
255+
expected_type = allowed_fields[key]
256+
if not isinstance(value, expected_type):
257+
raise ValidationError(
258+
_("BSCP 配置项 {key} 类型错误,期望 {expected_type},实际为 {actual_type}").format(
259+
key=key,
260+
expected_type=expected_type.__name__,
261+
actual_type=type(value).__name__,
262+
)
263+
)
264+
265+
# 如果是路径类型的字段,进行安全校验
266+
if key in ["config_path", "log_path", "data_path"]:
267+
if isinstance(value, str):
268+
# 检查危险字符
269+
for pattern in dangerous_patterns:
270+
if pattern in value:
271+
raise ValidationError(
272+
_("BSCP 配置项 {key} 包含不安全的字符: {pattern}").format(
273+
key=key, pattern=pattern
274+
)
275+
)
276+
277+
# 检查绝对路径
278+
if not value.startswith("/"):
279+
raise ValidationError(
280+
_("BSCP 配置项 {key} 必须是绝对路径").format(key=key)
281+
)
282+
283+
return bscp_config
284+
285+
32286
class ListSerializer(serializers.ModelSerializer):
33287
"""
34288
AP返回数据
@@ -120,6 +374,18 @@ class ZKSerializer(serializers.Serializer):
120374
callback_url = serializers.CharField(label=_("节点管理内网回调地址"), required=False, allow_blank=True)
121375

122376
def validate(self, data):
377+
# 校验 agent_config 的安全性
378+
if "agent_config" in data:
379+
data["agent_config"] = validate_agent_config(data["agent_config"])
380+
381+
# 校验 port_config 的安全性
382+
if "port_config" in data:
383+
data["port_config"] = validate_port_config(data["port_config"])
384+
385+
# 校验 bscp_config 的安全性
386+
if "bscp_config" in data and data["bscp_config"]:
387+
data["bscp_config"] = validate_bscp_config(data["bscp_config"])
388+
123389
gse_version_list: List[str] = list(set(AccessPoint.objects.values_list("gse_version", flat=True)))
124390
# 存量接入点版本全部为V2新建/更新版本也为V2版本
125391
if GseVersion.V1.value not in gse_version_list:

apps/node_man/serializers/install_channel.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,42 @@
1414
from apps.utils import basic
1515

1616

17+
class UpstreamServersSerializer(serializers.Serializer):
18+
"""
19+
上游节点配置序列化器
20+
"""
21+
taskserver = serializers.ListField(
22+
child=serializers.CharField(),
23+
label=_("任务服务器列表"),
24+
required=False,
25+
default=[]
26+
)
27+
btfileserver = serializers.ListField(
28+
child=serializers.CharField(),
29+
label=_("BT文件服务器列表"),
30+
required=False,
31+
default=[]
32+
)
33+
dataserver = serializers.ListField(
34+
child=serializers.CharField(),
35+
label=_("数据服务器列表"),
36+
required=False,
37+
default=[]
38+
)
39+
40+
def validate_taskserver(self, value):
41+
"""校验任务服务器列表"""
42+
return [basic.exploded_ip(ip) for ip in value]
43+
44+
def validate_btfileserver(self, value):
45+
"""校验BT文件服务器列表"""
46+
return [basic.exploded_ip(ip) for ip in value]
47+
48+
def validate_dataserver(self, value):
49+
"""校验数据服务器列表"""
50+
return [basic.exploded_ip(ip) for ip in value]
51+
52+
1753
class BaseSerializer(serializers.Serializer):
1854
"""
1955
用于安装节点管理校验
@@ -29,18 +65,10 @@ class UpdateSerializer(BaseSerializer):
2965

3066
name = serializers.CharField(label=_("安装通道名称"))
3167
jump_servers = serializers.ListField(label=_("跳板机节点"))
32-
upstream_servers = serializers.DictField(label=_("上游节点"))
68+
upstream_servers = UpstreamServersSerializer(label=_("上游节点"))
3369
hidden = serializers.BooleanField(label=_("是否隐藏"), default=False)
3470

3571
def validate(self, attrs):
3672
attrs["jump_servers"] = [basic.exploded_ip(jump_server) for jump_server in attrs["jump_servers"]]
37-
attrs["upstream_servers"]["taskserver"] = [
38-
basic.exploded_ip(taskserver) for taskserver in attrs["upstream_servers"]["taskserver"]
39-
]
40-
attrs["upstream_servers"]["btfileserver"] = [
41-
basic.exploded_ip(taskserver) for taskserver in attrs["upstream_servers"]["btfileserver"]
42-
]
43-
attrs["upstream_servers"]["dataserver"] = [
44-
basic.exploded_ip(taskserver) for taskserver in attrs["upstream_servers"]["dataserver"]
45-
]
73+
# upstream_servers 的校验已经在 UpstreamServersSerializer 中处理
4674
return attrs

0 commit comments

Comments
 (0)