Merge branch 'pr@dev@fix_sec' of github.com:jumpserver/jumpserver into pr@dev@fix_sec

This commit is contained in:
ibuler
2026-02-02 19:02:13 +08:00
9 changed files with 52 additions and 9 deletions

View File

@@ -18,6 +18,7 @@ from rest_framework.response import Response
from common.api import CommonApiMixin
from common.const.http import GET, POST
from common.drf.filters import DatetimeRangeFilterBackend
from common.drf.throttling import FileTransferThrottle
from common.permissions import IsServiceAccount
from common.plugins.es import QuerySet as ESQuerySet
from common.sessions.cache import user_session_manager
@@ -111,6 +112,7 @@ class FTPLogViewSet(OrgModelViewSet):
@action(
methods=[GET], detail=True, permission_classes=[RBACPermission, ],
throttle_classes=[FileTransferThrottle],
url_path='file/download'
)
def download(self, request, *args, **kwargs):
@@ -133,7 +135,9 @@ class FTPLogViewSet(OrgModelViewSet):
)
return response
@action(methods=[POST], detail=True, permission_classes=[IsServiceAccount, ], serializer_class=FileSerializer)
@action(methods=[POST], detail=True, permission_classes=[IsServiceAccount, ],
throttle_classes=[FileTransferThrottle],
serializer_class=FileSerializer)
def upload(self, request, *args, **kwargs):
ftp_log = self.get_object()
serializer = self.get_serializer(data=request.data)

View File

@@ -352,7 +352,7 @@
</ul>
</div>
<div class="contact-form col-md-10 col-md-offset-1" style='float: none; overflow: hidden'>
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
<form id="login-form" autocomplete="off" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
{% if form.non_field_errors %}
@@ -362,9 +362,9 @@
{% endif %}
</div>
{% bootstrap_field form.username show_label=False %}
{% bootstrap_field form.username autocomplete="off" show_label=False %}
<div class="form-group {% if form.password.errors %} has-error {% endif %}">
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}"
<input type="password" autocomplete="off" class="form-control" id="password" placeholder="{% trans 'Password' %}"
required>
<input id="password-hidden" type="text" style="display:none"
name="{{ form.password.html_name }}">

View File

@@ -12,6 +12,7 @@ from rest_framework.renderers import BaseRenderer
from rest_framework.utils import encoders, json
from common.serializers import fields as common_fields
from common.serializers.fields import EncryptedField
from common.utils import get_logger
from .mixins import LogMixin
@@ -23,6 +24,8 @@ class BaseFileRenderer(LogMixin, BaseRenderer):
# 渲染模板标识, 导入、导出、更新模板: ['import', 'update', 'export']
template = 'export'
serializer = None
# 敏感字段名称,这些字段不允许导出
secret_field_names = ("password", "token", "secret", "key", "private_key", "passphrase")
@staticmethod
def _check_validation_data(data):
@@ -48,6 +51,18 @@ class BaseFileRenderer(LogMixin, BaseRenderer):
disposition = 'attachment; filename="{}"'.format(filename)
response['Content-Disposition'] = disposition
def is_secret_field(self, field):
"""检查字段是否为敏感字段,敏感字段不允许导出"""
# 检查字段类型是否为 EncryptedField
if isinstance(field, EncryptedField):
return True
# 检查字段名是否包含敏感关键词
field_name = field.field_name.lower()
for secret_name in self.secret_field_names:
if secret_name in field_name:
return True
return False
def get_rendered_fields(self):
fields = self.serializer.fields
meta = getattr(self.serializer, 'Meta', None)
@@ -62,6 +77,8 @@ class BaseFileRenderer(LogMixin, BaseRenderer):
fields_unexport = getattr(meta, 'fields_unexport', [])
fields = [v for v in fields if v.field_name not in fields_unexport]
# 过滤敏感字段,禁止口令、密钥等敏感信息导出
fields = [v for v in fields if not self.is_secret_field(v)]
return fields
@staticmethod

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from rest_framework.throttling import SimpleRateThrottle
__all__ = ['RateThrottle', 'FileTransferThrottle']
class RateThrottle(SimpleRateThrottle):
@@ -32,3 +34,17 @@ class RateThrottle(SimpleRateThrottle):
'scope': self.scope,
'ident': ident
}
class FileTransferThrottle(SimpleRateThrottle):
"""
文件上传下载限流防止DOS攻击
"""
scope = 'file_transfer'
def get_cache_key(self, request, view):
if request.user and request.user.is_authenticated:
ident = request.user.pk
else:
ident = self.get_ident(request)
return self.cache_format % {'scope': self.scope, 'ident': ident}

View File

@@ -226,6 +226,9 @@ class Config(dict):
'THROTTLE_RATES_USER': '180/min',
'THROTTLE_RATES_SERVICE_ACCOUNT': '300/min',
# 文件上传下载限流 (防止DOS攻击)
'THROTTLE_FILE_TRANSFER': '50/hour',
# Security
'X_FRAME_OPTIONS': 'SAMEORIGIN',
'VERIFY_EXTERNAL_SSL': True,

View File

@@ -45,6 +45,7 @@ REST_FRAMEWORK = {
'anon': CONFIG.THROTTLE_RATES_ANON,
'user': CONFIG.THROTTLE_RATES_USER,
'service_account': CONFIG.THROTTLE_RATES_SERVICE_ACCOUNT,
'file_transfer': CONFIG.THROTTLE_FILE_TRANSFER,
},
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',

View File

@@ -18,6 +18,7 @@ from rest_framework.views import APIView
from acls.models import LoginAssetACL
from assets.models import Asset
from common.const.http import POST
from common.drf.throttling import FileTransferThrottle
from common.permissions import IsValidUser
from common.utils import get_request_ip_or_data
from ops.celery import app
@@ -171,7 +172,9 @@ class JobViewSet(LoginAssetACLCheckMixin, OrgBulkModelViewSet):
return exceeds_limit_files
@action(methods=[POST], detail=False, serializer_class=FileSerializer,
permission_classes=[IsValidUser, ], url_path='upload')
permission_classes=[IsValidUser, ],
throttle_classes=[FileTransferThrottle],
url_path='upload')
def upload(self, request, *args, **kwargs):
uploaded_files = request.FILES.getlist('files')
serializer = self.get_serializer(data=request.data)

View File

@@ -25,6 +25,7 @@ from common.const.http import GET, POST
from common.drf.filters import BaseFilterSet
from common.drf.filters import DatetimeRangeFilterBackend
from common.drf.renders import PassthroughRenderer
from common.drf.throttling import FileTransferThrottle
from common.permissions import IsServiceAccount
from common.storage.replay import ReplayStorageHandler, SessionPartReplayStorageHandler
from common.utils import data_to_json, is_uuid, i18n_fmt
@@ -127,6 +128,7 @@ class SessionViewSet(OrgBulkModelViewSet):
return file
@action(methods=[GET], detail=True, renderer_classes=(PassthroughRenderer,), url_path='replay/download',
throttle_classes=[FileTransferThrottle],
url_name='replay-download')
def download(self, request, *args, **kwargs):
session = self.get_object()

View File

@@ -15,7 +15,7 @@
{% endif %}
{% csrf_token %}
<div class="form-input form-group">
<input type="password" id="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
<input type="password" id="password" autocomplete="off" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
</div>
<button type="submit" class="btn btn-primary">{% trans 'Confirm' %}</button>
</form>
@@ -32,6 +32,3 @@
});
</script>
{% endblock %}