Skip to content

Commit a89e22b

Browse files
narasuxpiglei
andauthored
feat: enhance file upload, git url validation and command handling (#2835)
Co-authored-by: piglei <piglei2007@gmail.com>
1 parent 38f769c commit a89e22b

17 files changed

Lines changed: 516 additions & 67 deletions

File tree

apiserver/paasng/paasng/misc/tools/smart_app/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
from paasng.utils.i18n.serializers import TranslatedCharField
2525
from paasng.utils.models import OrderByField
2626
from paasng.utils.serializers import StringArrayField
27+
from paasng.utils.validators import SafeFilenameValidator
2728

2829
from .constants import SourceCodeOriginType
2930

3031

3132
class ToolPackageStashInputSLZ(serializers.Serializer):
3233
"""Upload source package SLZ"""
3334

34-
package = serializers.FileField(help_text="待构建的应用源码包")
35+
package = serializers.FileField(help_text="待构建的应用源码包", validators=[SafeFilenameValidator()])
3536

3637

3738
class BaseSmartBuildSLZ(serializers.Serializer):

apiserver/paasng/paasng/misc/tools/smart_app/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from paasng.platform.smart_app.services.prepared import PreparedSourcePackage
3737
from paasng.platform.sourcectl.utils import generate_temp_dir
3838
from paasng.utils.error_codes import error_codes
39-
from paasng.utils.views import get_filepath
39+
from paasng.utils.views import save_uploaded_file
4040

4141
from .build import SmartBuildCoordinator, SmartBuildTaskRunner, create_smart_build_record
4242
from .filters import SmartBuildRecordFilterBackend
@@ -72,7 +72,7 @@ def upload(self, request):
7272
slz.is_valid(raise_exception=True)
7373

7474
with generate_temp_dir() as tmp_dir:
75-
filepath = get_filepath(slz.validated_data["package"], str(tmp_dir))
75+
filepath = save_uploaded_file(slz.validated_data["package"], str(tmp_dir))
7676
stat = SourcePackageStatReader(filepath).read()
7777

7878
if not stat.meta_info:

apiserver/paasng/paasng/platform/smart_app/serializers.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
# We undertake not to change the open source license (MIT license) applicable
1616
# to the current version of the project delivered to anyone in the future.
1717

18-
import re
19-
20-
from django.utils.translation import gettext_lazy as _
2118
from rest_framework import serializers
2219

2320
from paasng.core.tenant.constants import AppTenantMode
@@ -26,6 +23,7 @@
2623
from paasng.platform.declarative.application.validations.v2 import MarketSLZ, ModuleDescriptionSLZ
2724
from paasng.platform.declarative.constants import DiffType
2825
from paasng.utils.i18n.serializers import TranslatedCharField
26+
from paasng.utils.validators import SafeFilenameValidator
2927

3028

3129
class AppDescriptionSLZ(serializers.Serializer):
@@ -41,18 +39,19 @@ class AppDescriptionSLZ(serializers.Serializer):
4139
class PackageStashRequestSLZ(serializers.Serializer):
4240
"""Handle S-mart application uploads"""
4341

44-
package = serializers.FileField(help_text="应用源码包")
42+
package = serializers.FileField(
43+
help_text="应用源码包",
44+
validators=[
45+
SafeFilenameValidator(
46+
name_pattern=r"[a-zA-Z0-9\-_.]+",
47+
name_pattern_message="格式错误,只能包含字母(a-zA-Z)、数字(0-9)和半角连接符(-)、下划线(_)和点(.)",
48+
)
49+
],
50+
)
4551
app_tenant_mode = serializers.ChoiceField(
4652
help_text="应用租户模式", choices=AppTenantMode.get_choices(), default=None
4753
)
4854

49-
def validate_package(self, package):
50-
if not re.fullmatch("[a-zA-Z0-9-_. ]+", package.name):
51-
raise serializers.ValidationError(
52-
{"invalid": _("格式错误,只能包含字母(a-zA-Z)、数字(0-9)和半角连接符(-)、下划线(_)、空格( )和点(.)")}
53-
)
54-
return package
55-
5655

5756
class PackageStashConfirmRequestSLZ(AppBasicInfoMixin):
5857
"""Handle S-mart application confirm after upload

apiserver/paasng/paasng/platform/smart_app/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
from paasng.platform.sourcectl.utils import generate_temp_dir, generate_temp_file
6969
from paasng.utils.error_codes import error_codes
7070
from paasng.utils.moby_distribution.registry.exceptions import RequestError as RequestRegistryError
71-
from paasng.utils.views import get_filepath
71+
from paasng.utils.views import save_uploaded_file
7272

7373
logger = logging.getLogger(__name__)
7474

@@ -109,7 +109,7 @@ def upload(self, request):
109109
)
110110

111111
with generate_temp_dir() as download_dir:
112-
filepath = get_filepath(package_fp, str(download_dir))
112+
filepath = save_uploaded_file(package_fp, str(download_dir))
113113

114114
stat = SourcePackageStatReader(filepath).read()
115115

@@ -321,7 +321,7 @@ def stash(self, request, code):
321321
)
322322

323323
with generate_temp_dir() as download_dir:
324-
filepath = get_filepath(package_fp, download_dir)
324+
filepath = save_uploaded_file(package_fp, download_dir)
325325

326326
stat = SourcePackageStatReader(filepath).read()
327327

apiserver/paasng/paasng/platform/sourcectl/git/client.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from django.utils.encoding import force_str
3030

3131
from paasng.platform.sourcectl.exceptions import RequestTimeOutError
32+
from paasng.utils.validators import validate_repo_url
3233

3334
logger = logging.getLogger(__name__)
3435

@@ -37,6 +38,10 @@ class GitCommandExecutionError(Exception):
3738
"""执行 git 命令时发生错误"""
3839

3940

41+
class GitURLValidationError(GitCommandExecutionError):
42+
"""Git 仓库 URL 安全校验失败"""
43+
44+
4045
@dataclass
4146
class Ref:
4247
name: str
@@ -50,22 +55,36 @@ def __str__(self):
5055

5156

5257
class GitCommand:
58+
"""Git 命令封装。
59+
60+
:param args: Git 子命令选项参数,位于 ``--`` 之前。
61+
:param end_of_options_args: ``--`` 之后的参数(URL、路径、分支名等),
62+
这些参数不会被 git 解释为选项,即使它们以 ``-`` 开头。
63+
构建命令时会自动在前面插入 ``--`` 分隔符。
64+
"""
65+
5366
def __init__(
5467
self,
5568
git_filepath: str,
5669
command: str,
57-
args: Optional[List[str]] = None,
70+
args: List[str] | None = None,
71+
end_of_options_args: List[str] | None = None,
5872
cwd: str = "",
5973
envs: Optional[Dict] = None,
6074
):
6175
self.git_filepath = git_filepath
6276
self.command = command
6377
self.args = args or []
78+
self.end_of_options_args = end_of_options_args or []
6479
self.cwd = cwd
6580
self.envs = envs or {}
6681

6782
def to_cmd(self, obscure: bool = False) -> List[str]:
68-
return [self.git_filepath, self.command, *self.args]
83+
cmd = [self.git_filepath, self.command, *self.args]
84+
if self.end_of_options_args:
85+
cmd.append("--")
86+
cmd.extend(self.end_of_options_args)
87+
return cmd
6988

7089
def get_sensitive_texts(self) -> List[str]:
7190
"""Get sensitive texts in the current command, the texts must not be displayed
@@ -93,13 +112,14 @@ def __init__(
93112
SYNOPSIS
94113
git clone [--bare] [--] <repository> [<target_directory>]
95114
"""
96-
super().__init__(git_filepath, "clone", args, cwd, envs)
115+
super().__init__(git_filepath=git_filepath, command="clone", args=args, cwd=cwd, envs=envs)
97116
self.repository = repository
98117
self.target_directory = target_directory
99118

100119
def to_cmd(self, obscure: bool = False) -> List[str]:
101120
""""""
102121
cmd = super().to_cmd(obscure=obscure)
122+
cmd.append("--")
103123
cmd.extend([str(self.repository.obscure()) if obscure else str(self.repository), self.target_directory])
104124
return cmd
105125

@@ -128,22 +148,26 @@ class GitClient:
128148

129149
def checkout(self, path: Path, target: str) -> str:
130150
"""切换分支或tag"""
131-
command = GitCommand(git_filepath=self._git_filepath, command="checkout", args=[target], cwd=str(path))
151+
command = GitCommand(
152+
git_filepath=self._git_filepath, command="checkout", end_of_options_args=[target], cwd=str(path)
153+
)
132154
return self.run(command)
133155

134156
def clone(
135157
self,
136158
url: Union[str, MutableURL],
137159
path: Path,
138-
envs: Optional[dict] = None,
139-
depth: Optional[int] = None,
140-
branch: Optional[str] = None,
160+
envs: Dict | None = None,
161+
depth: int | None = None,
162+
branch: str | None = None,
141163
) -> str:
142164
"""克隆仓库
143165
144166
:param url: 仓库地址
145167
:param path: 存储路径
146168
:param envs: 环境变量
169+
:param depth: 克隆深度
170+
:param branch: 克隆分支
147171
:return: 返回 clone 结果
148172
"""
149173
args = []
@@ -182,8 +206,7 @@ def clone_no_blob(self, url: Union[str, MutableURL], path: Path) -> str:
182206
def list_remote(self, url: Union[str, MutableURL]) -> List[Tuple[str, str]]:
183207
"""通过 ls-remote 命令,直接获取远端仓库的 branch 与 tag 等信息。该命令不依赖本地文件系统。
184208
185-
:param path: 项目路径
186-
:param remote: 远端名称,默认为 origin
209+
:param url: 仓库地址
187210
:return: 命令执行结果,包含 (commit_id, ref) 的列表
188211
"""
189212
results = []
@@ -206,12 +229,20 @@ def list_remote(self, url: Union[str, MutableURL]) -> List[Tuple[str, str]]:
206229
def list_remote_raw(self, url: Union[str, MutableURL]) -> str:
207230
"""通过 ls-remote 命令,直接获取远端仓库的 branch 与 tag 等信息。返回命令原始执行结果。
208231
209-
:param path: 项目路径
210-
:param remote: 远端名称,默认为 origin
232+
:param url: 仓库地址
211233
:return: 命令原始执行结果
212234
"""
235+
# 在执行 Git 操作前校验 URL 安全性,防止历史脏数据绕过
236+
url_str = str(url)
237+
try:
238+
validate_repo_url(url_str)
239+
except ValueError as e:
240+
raise GitURLValidationError(str(e))
241+
213242
# The "cwd" is pointless for running this command, always use current dir.
214-
command = GitCommand(git_filepath=self._git_filepath, command="ls-remote", args=[str(url)], cwd=os.getcwd())
243+
command = GitCommand(
244+
git_filepath=self._git_filepath, command="ls-remote", end_of_options_args=[url_str], cwd=os.getcwd()
245+
)
215246
return self.run(command)
216247

217248
def list_refs(self, path: Path) -> Generator[Ref, None, None]:

apiserver/paasng/paasng/platform/sourcectl/serializers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from paasng.platform.sourcectl.type_specs import BkSvnSourceTypeSpec
2828
from paasng.platform.sourcectl.validators import validate_download_url
2929
from paasng.utils.serializers import DecryptableJSONField, SourceControlField, UserNameField, VerificationCodeField
30-
from paasng.utils.validators import validate_image_repo, validate_repo_url
30+
from paasng.utils.validators import SafeFilenameValidator, validate_image_repo, validate_repo_url
3131

3232
logger = logging.getLogger(__name__)
3333

@@ -206,5 +206,13 @@ def validate_package_url(self, value: str) -> str:
206206

207207

208208
class SourcePackageUploadViaFileSLZ(serializers.Serializer):
209-
package = serializers.FileField(help_text="源码包文件")
209+
package = serializers.FileField(
210+
help_text="源码包文件",
211+
validators=[
212+
SafeFilenameValidator(
213+
name_pattern=r"[a-zA-Z0-9\-_.]+",
214+
name_pattern_message="格式错误,只能包含字母(a-zA-Z)、数字(0-9)和半角连接符(-)、下划线(_)和点(.)",
215+
)
216+
],
217+
)
210218
allow_overwrite = serializers.BooleanField(help_text="是否允许覆盖原有的源码包", default=False, allow_null=True)

apiserver/paasng/paasng/platform/sourcectl/svn/admin.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ def del_authz(self, repo_path, user_or_group, type_id):
6262
raise NotImplementedError
6363

6464

65+
# FIXME: 该服务数年前已下线,后续确认能否整个删除
6566
class BaseRealSvnAuthClient(BaseSvnAuthClient):
66-
SVN_SECRET = "32fc6114554e3c53d5952594510021e2"
67+
SVN_SECRET = ""
6768
SVN_OPERATE_ERROR_NOTIFIER = settings.ADMIN_USERNAME
6869
DUMMY = True
6970
TIMEOUT = 60
70-
SSL_VERIFY = False
71+
SSL_VERIFY = True
7172

7273
BASE_SVN_ADD_USER = "{admin_url}svn_add/user/"
7374
BASE_SVN_MOD_COMMON = "{admin_url}svn_mod/common/"

apiserver/paasng/paasng/settings/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ def minimum_database_version(self):
129129

130130
DEBUG = settings.get("DEBUG", False)
131131

132-
SESSION_COOKIE_HTTPONLY = False
133-
134132
RUNNING_TESTS = "test" in sys.argv or "pytest" in sys.argv[0] or "PYTEST_XDIST_TESTRUNUID" in os.environ
135133

136134
INSTALLED_APPS = [

0 commit comments

Comments
 (0)