Skip to content

Commit 63f1fcd

Browse files
Merge remote-tracking branch 'upstream/v2' into upgrade_jsonpath-ng_to_1.8.0
2 parents 194dc2e + 2eaa067 commit 63f1fcd

File tree

33 files changed

+973
-615
lines changed

33 files changed

+973
-615
lines changed

apps/application/serializers/application.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,8 +1036,8 @@ def edit(self, instance: Dict, with_valid=True):
10361036
if 'work_flow' in instance:
10371037
# 修改语音配置相关
10381038
self.update_work_flow_model(instance)
1039-
if 'mcp_servers' in instance:
1040-
ToolExecutor().validate_mcp_transport(instance.get('mcp_servers'))
1039+
if 'mcp_servers' in instance and len(instance.get('mcp_servers', {})) > 0:
1040+
ToolExecutor().validate_mcp_transport(json.dumps(instance.get('mcp_servers')))
10411041
update_keys = ['name', 'desc', 'model_id', 'multiple_rounds_dialogue', 'prologue', 'status',
10421042
'knowledge_setting', 'model_setting', 'problem_optimization', 'dialogue_number',
10431043
'stt_model_id', 'tts_model_id', 'tts_model_enable', 'stt_model_enable', 'tts_type',

apps/application/serializers/application_chat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ def to_row(row: Dict):
193193
def reset_value(value):
194194
if isinstance(value, str):
195195
value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
196+
if value.startswith(('=', '+', '-', '@')):
197+
value = "'" + value
196198
if isinstance(value, datetime.datetime):
197199
eastern = pytz.timezone(TIME_ZONE)
198200
c = datetime.timezone(eastern._utcoffset)

apps/common/utils/tool_code.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def init_sandbox_dir():
7171
os.remove(sandbox_conf_file_path)
7272
banned_hosts = CONFIG.get("SANDBOX_PYTHON_BANNED_HOSTS", '').strip()
7373
allow_dl_paths = CONFIG.get("SANDBOX_PYTHON_ALLOW_DL_PATHS",'').strip()
74+
allow_dl_open = CONFIG.get("SANDBOX_PYTHON_ALLOW_DL_OPEN",'0')
7475
allow_subprocess = CONFIG.get("SANDBOX_PYTHON_ALLOW_SUBPROCESS", '0')
7576
allow_syscall = CONFIG.get("SANDBOX_PYTHON_ALLOW_SYSCALL", '0')
7677
if banned_hosts:
@@ -81,6 +82,7 @@ def init_sandbox_dir():
8182
with open(sandbox_conf_file_path, "w") as f:
8283
f.write(f"SANDBOX_PYTHON_BANNED_HOSTS={banned_hosts}\n")
8384
f.write(f"SANDBOX_PYTHON_ALLOW_DL_PATHS={','.join(sorted(set(filter(None, sys.path + _sandbox_python_sys_path + allow_dl_paths.split(',')))))}\n")
85+
f.write(f"SANDBOX_PYTHON_ALLOW_DL_OPEN={allow_dl_open}\n")
8486
f.write(f"SANDBOX_PYTHON_ALLOW_SUBPROCESS={allow_subprocess}\n")
8587
f.write(f"SANDBOX_PYTHON_ALLOW_SYSCALL={allow_syscall}\n")
8688
os.system(f"chmod -R 550 {_sandbox_path}")

apps/tools/serializers/tool.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,17 +613,19 @@ def one(self):
613613
'size': skill_file.file_size,
614614
} if skill_file else None
615615
work_flow = {}
616+
is_publish = False
616617
if tool.tool_type == 'WORKFLOW':
617618
tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=tool.id).first()
618619
if tool_workflow:
619620
work_flow = tool_workflow.work_flow
620-
621+
is_publish = tool_workflow.is_publish
621622
return {
622623
**ToolModelSerializer(tool).data,
623624
'init_params': tool.init_params if tool.init_params else {},
624625
'nick_name': nick_name,
625626
'fileList': [skill_file_dict] if tool.tool_type == 'SKILL' else [],
626-
'work_flow': work_flow
627+
'work_flow': work_flow,
628+
'is_publish': is_publish
627629
}
628630

629631
def export(self):

apps/users/serializers/login.py

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
@desc:
88
"""
99
import base64
10-
import datetime
1110
import json
11+
import logging
1212

1313
from captcha.image import ImageCaptcha
1414
from django.core import signing
@@ -23,9 +23,10 @@
2323
from common.database_model_manage.database_model_manage import DatabaseModelManage
2424
from common.exception.app_exception import AppApiException
2525
from common.utils.common import password_encrypt, get_random_chars
26-
from common.utils.rsa_util import encrypt, decrypt
26+
from common.utils.rsa_util import decrypt
2727
from maxkb.const import CONFIG
2828
from users.models import User
29+
from common.utils.logger import maxkb_logger
2930

3031

3132
class LoginRequest(serializers.Serializer):
@@ -48,26 +49,34 @@ class LoginResponse(serializers.Serializer):
4849

4950

5051
def record_login_fail(username: str, expire: int = 600):
51-
"""记录登录失败次数"""
52+
"""记录登录失败次数(原子)返回当前失败计数"""
5253
if not username:
53-
return
54+
return 0
5455
fail_key = system_get_key(f'system_{username}')
55-
fail_count = cache.get(fail_key, version=system_version)
56-
if fail_count is None:
56+
try:
57+
fail_count = cache.incr(fail_key, 1, version=system_version)
58+
except ValueError:
59+
# key 不存在,初始化并设置过期
5760
cache.set(fail_key, 1, timeout=expire, version=system_version)
58-
else:
59-
cache.incr(fail_key, 1, version=system_version)
61+
fail_count = 1
62+
return fail_count
6063

6164

6265
def record_login_fail_lock(username: str, expire: int = 10):
66+
"""
67+
使用 cache.incr 保证原子递增,并在不存在时初始化计数器并返回当前值。
68+
这里的计数器用于判断是否应当进入“锁定”分支,避免依赖非原子 get -> set 的组合。
69+
"""
6370
if not username:
64-
return
71+
return 0
6572
fail_key = system_get_key(f'system_{username}_lock_count')
66-
fail_count = cache.get(fail_key, version=system_version)
67-
if fail_count is None:
73+
try:
74+
fail_count = cache.incr(fail_key, 1, version=system_version)
75+
except ValueError:
76+
# key 不存在,初始化并设置过期(分钟转秒)
6877
cache.set(fail_key, 1, timeout=expire * 60, version=system_version)
69-
else:
70-
cache.incr(fail_key, 1, version=system_version)
78+
fail_count = 1
79+
return fail_count
7180

7281

7382
class LoginSerializer(serializers.Serializer):
@@ -93,8 +102,16 @@ def login(instance):
93102
encrypted_data = instance.get("encryptedData", "")
94103

95104
if encrypted_data:
96-
decrypted_data = json.loads(decrypt(encrypted_data))
97-
instance.update(decrypted_data)
105+
try:
106+
decrypted_raw = decrypt(encrypted_data)
107+
# decrypt 可能返回非 JSON 字符串,防护解析异常
108+
decrypted_data = json.loads(decrypted_raw) if decrypted_raw else {}
109+
if isinstance(decrypted_data, dict):
110+
instance.update(decrypted_data)
111+
except Exception as e:
112+
maxkb_logger.exception("Failed to decrypt/parse encryptedData for user %s: %s", username, e)
113+
raise AppApiException(500, _("Invalid encrypted data"))
114+
98115
try:
99116
LoginRequest(data=instance).is_valid(raise_exception=True)
100117
except serializers.ValidationError:
@@ -128,7 +145,7 @@ def login(instance):
128145
LoginSerializer._validate_captcha(username, captcha)
129146

130147
# 验证用户凭据
131-
user = QuerySet(User).filter(
148+
user = User.objects.filter(
132149
username=username,
133150
password=password_encrypt(password)
134151
).first()
@@ -190,34 +207,61 @@ def _validate_captcha(username: str, captcha: str) -> None:
190207

191208
@staticmethod
192209
def _handle_failed_login(username: str, is_license_valid: bool, failed_attempts: int, lock_time: int) -> None:
193-
"""处理登录失败"""
194-
record_login_fail(username)
195-
record_login_fail_lock(username, lock_time)
210+
"""处理登录失败
211+
212+
修复要点:
213+
- 使用 record_login_fail / record_login_fail_lock 两个原子 incr 来记录失败;
214+
- 不再依赖精确等于 0 的比较来触发锁,而是基于原子计数 >= 阈值来决定进入锁定分支;
215+
- 使用 cache.add 原子创建锁键,cache.add 保证只有第一个成功创建者可写入该键;
216+
其他并发到达的请求若发现计数已到达阈值也应当返回“已锁定”响应,避免出现绕过。
217+
"""
218+
# 记录普通失败计数(供验证码触发使用)
219+
try:
220+
record_login_fail(username)
221+
except Exception:
222+
maxkb_logger.exception("Failed to record login fail for user %s", username)
223+
224+
# 记录用于锁定判断的失败计数(按 lock_time 作为初始化过期分钟)
225+
lock_fail_count = 0
226+
try:
227+
lock_fail_count = record_login_fail_lock(username, lock_time)
228+
except Exception:
229+
maxkb_logger.exception("Failed to record lock fail count for user %s", username)
196230

231+
# 如果不是企业版或禁用锁定功能,直接返回(但计数已经记录)
197232
if not is_license_valid or failed_attempts <= 0:
198233
return
199234

200-
fail_count = cache.get(system_get_key(f'system_{username}_lock_count'), version=system_version) or 0
201-
remain_attempts = failed_attempts - fail_count
202-
203-
if remain_attempts > 0:
235+
# 当计数小于阈值,告知剩余尝试次数
236+
if lock_fail_count < failed_attempts:
237+
remain_attempts = failed_attempts - lock_fail_count
204238
raise AppApiException(
205239
1005,
206240
_("Login failed %s times, account will be locked, you have %s more chances !") % (
207241
failed_attempts, remain_attempts
208242
)
209243
)
210-
elif remain_attempts == 0:
211-
cache.set(
244+
245+
# 当计数达到或超过阈值时,尝试原子创建锁键;无论 cache.add 返回 True/False,都返回已锁定响应,
246+
# 因为若为 False 说明其他并发请求已将账户标记为锁定,行为应一致。
247+
try:
248+
locked = cache.add(
212249
system_get_key(f'system_{username}_lock'),
213250
1,
214251
timeout=lock_time * 60,
215252
version=system_version
216253
)
217-
raise AppApiException(
218-
1005,
219-
_("This account has been locked for %s minutes, please try again later") % lock_time
220-
)
254+
if locked:
255+
maxkb_logger.info("Account %s locked by setting cache key", username)
256+
else:
257+
maxkb_logger.info("Account %s lock key already present (another request set it)", username)
258+
except Exception:
259+
maxkb_logger.exception("Failed to set lock key for user %s", username)
260+
261+
raise AppApiException(
262+
1005,
263+
_("This account has been locked for %s minutes, please try again later") % lock_time
264+
)
221265

222266

223267
class CaptchaResponse(serializers.Serializer):

installer/sandbox.c

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
#include <pty.h>
2323
#include <stdint.h>
2424
#include <stdbool.h>
25+
#include <execinfo.h>
2526

2627
#define CONFIG_FILE ".sandbox.conf"
2728
#define KEY_BANNED_HOSTS "SANDBOX_PYTHON_BANNED_HOSTS"
2829
#define KEY_ALLOW_DL_PATHS "SANDBOX_PYTHON_ALLOW_DL_PATHS"
30+
#define KEY_ALLOW_DL_OPEN "SANDBOX_PYTHON_ALLOW_DL_OPEN"
2931
#define KEY_ALLOW_SUBPROCESS "SANDBOX_PYTHON_ALLOW_SUBPROCESS"
3032
#define KEY_ALLOW_SYSCALL "SANDBOX_PYTHON_ALLOW_SYSCALL"
3133

3234
static char *banned_hosts = NULL;
3335
static char *allow_dl_paths = NULL;
36+
static int allow_dl_open = 0;
3437
static int allow_subprocess = 0; // 默认禁止
3538
static int allow_syscall = 0;
3639

@@ -39,6 +42,7 @@ static void load_sandbox_config() {
3942
if (dladdr((void *)load_sandbox_config, &info) == 0 || !info.dli_fname) {
4043
banned_hosts = strdup("");
4144
allow_dl_paths = strdup("");
45+
allow_dl_open = 0;
4246
allow_subprocess = 0;
4347
allow_syscall = 0;
4448
return;
@@ -53,6 +57,7 @@ static void load_sandbox_config() {
5357
if (!fp) {
5458
banned_hosts = strdup("");
5559
allow_dl_paths = strdup("");
60+
allow_dl_open = 0;
5661
allow_subprocess = 0;
5762
allow_syscall = 0;
5863
return;
@@ -62,6 +67,7 @@ static void load_sandbox_config() {
6267
if (allow_dl_paths) { free(allow_dl_paths); allow_dl_paths = NULL; }
6368
banned_hosts = strdup("");
6469
allow_dl_paths = strdup("");
70+
allow_dl_open = 0;
6571
allow_subprocess = 0;
6672
allow_syscall = 0;
6773
while (fgets(line, sizeof(line), fp)) {
@@ -80,6 +86,8 @@ static void load_sandbox_config() {
8086
} else if (strcmp(key, KEY_ALLOW_DL_PATHS) == 0) {
8187
free(allow_dl_paths);
8288
allow_dl_paths = strdup(value); // 逗号分隔字符串
89+
} else if (strcmp(key, KEY_ALLOW_DL_OPEN) == 0) {
90+
allow_dl_open = atoi(value);
8391
} else if (strcmp(key, KEY_ALLOW_SUBPROCESS) == 0) {
8492
allow_subprocess = atoi(value);
8593
} else if (strcmp(key, KEY_ALLOW_SYSCALL) == 0) {
@@ -503,6 +511,7 @@ long syscall(long number, ...) {
503511
case SYS_accept:
504512
case SYS_accept4:
505513
case SYS_sendto:
514+
case SYS_sendmsg:
506515
case SYS_recvmsg:
507516
case SYS_getsockopt:
508517
case SYS_setsockopt:
@@ -519,6 +528,7 @@ long syscall(long number, ...) {
519528
#endif
520529
case SYS_fchmodat:
521530
case SYS_mprotect:
531+
case SYS_pkey_mprotect:
522532
#ifdef SYS_open
523533
case SYS_open:
524534
#endif
@@ -533,6 +543,9 @@ long syscall(long number, ...) {
533543
case SYS_shmget:
534544
case SYS_shmctl:
535545
case SYS_prctl:
546+
case SYS_io_uring_setup:
547+
case SYS_io_uring_enter:
548+
case SYS_io_uring_register:
536549
if (!allow_access_syscall()) {
537550
throw_permission_denied_err(true, "access syscall %ld", number);
538551
}
@@ -543,7 +556,24 @@ long syscall(long number, ...) {
543556
/**
544557
* 限制加载动态链接库
545558
*/
546-
static int is_in_allow_dl_paths(const char *filename) {
559+
static int called_from_python_import() {
560+
if (allow_dl_open) return 1;
561+
void *buf[32];
562+
int n = backtrace(buf, 32);
563+
for (int i = 0; i < n; i++) {
564+
Dl_info info;
565+
if (dladdr(buf[i], &info) && info.dli_sname) {
566+
if (strstr(info.dli_sname, "PyImport") ||
567+
strstr(info.dli_sname, "_PyImport")) {
568+
return 1;
569+
}
570+
}
571+
}
572+
throw_permission_denied_err(true, "open dynamic link library");
573+
return 0;
574+
}
575+
static int is_allow_dl(const char *filename) {
576+
if (!called_from_python_import()) return 0;
547577
if (!filename || !*filename) return 1;
548578
ensure_config_loaded();
549579
if (!allow_dl_paths || !*allow_dl_paths) return 0;
@@ -570,7 +600,7 @@ static int is_in_allow_dl_paths(const char *filename) {
570600
}
571601
void *dlopen(const char *filename, int flag) {
572602
RESOLVE_REAL(dlopen);
573-
if (is_sandbox_user() && !is_in_allow_dl_paths(filename)) {
603+
if (is_sandbox_user() && !is_allow_dl(filename)) {
574604
throw_permission_denied_err(true, "access file %s", filename);
575605
}
576606
return real_dlopen(filename, flag);
@@ -580,7 +610,7 @@ void *__dlopen(const char *filename, int flag) {
580610
}
581611
void *dlmopen(Lmid_t lmid, const char *filename, int flags) {
582612
RESOLVE_REAL(dlmopen);
583-
if (is_sandbox_user() && !is_in_allow_dl_paths(filename)) {
613+
if (is_sandbox_user() && !is_allow_dl(filename)) {
584614
throw_permission_denied_err(true, "access file %s", filename);
585615
}
586616
return real_dlmopen(lmid, filename, flags);
@@ -602,7 +632,7 @@ void* mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off) {
602632
throw_permission_denied_err(true, "mmap(readlink failed)");
603633
}
604634
real_path[n] = '\0';
605-
if (!is_in_allow_dl_paths(real_path)) {
635+
if (!is_allow_dl(real_path)) {
606636
throw_permission_denied_err(true, "mmap %s", real_path);
607637
}
608638
}

installer/start-maxkb.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,16 @@ fi
1111
mkdir -p /opt/maxkb/python-packages
1212

1313
rm -f /opt/maxkb-app/tmp/*
14+
15+
INIT_SHELL_DIR="/opt/maxkb/local/init-shells"
16+
if [ -d "$INIT_SHELL_DIR" ]; then
17+
find "$INIT_SHELL_DIR" -maxdepth 1 -type f -name "*.sh" | sort | while IFS= read -r f; do
18+
if bash "$f"; then
19+
echo "[OK] init-shell >>> $f"
20+
else
21+
echo "[ERROR] init-shell >>> $f failed with exit code $?" >&2
22+
fi
23+
done
24+
fi
25+
1426
python /opt/maxkb-app/main.py start

0 commit comments

Comments
 (0)