fix(marketplace): UUID-v4 whitelist on pack_id + stale-response guard (pr_agent #444 round 2)
pr_agent 提的两个真问题: 1. Security: arbitrary file write —— marketplace_install 用 backend-controlled pack_id 拼临时文件路径。若 backend 被攻陷返回路径遍历 id,可写客户端任意位置。 修:4 个 marketplace_* IPC 全部加 is_valid_session_id() UUID-v4 白名单 校验(跟 read_audio_recording 同套 boundary 校验)。 2. Race condition: 搜索 refresh 旧请求晚到覆盖新结果。用户连续输入时体验差。 修:useRef 单调递增 seq token,response 到了 check seq 是否最新,stale 直接丢。 cargo test 263 全过;tsc 0 error。
This commit is contained in:
1 parent
242f7ef8cd
commit
4af7fec82f
2 files changed
+25
-3
No files matched your search
@@ -2379,6 +2379,9 @@ pub async fn marketplace_detail(
|
||||
coord: CoordinatorState<'_>,
|
||||
pack_id: String,
|
||||
) -> Result<MarketplaceDetail, String> {
|
||||
if !is_valid_session_id(&pack_id) {
|
||||
return Err("invalid pack id".into());
|
||||
}
|
||||
let prefs = coord.prefs().get();
|
||||
let base = marketplace_url_from_prefs(&prefs);
|
||||
let client = reqwest::Client::new();
|
||||
@@ -2402,6 +2405,12 @@ pub async fn marketplace_install(
|
||||
coord: CoordinatorState<'_>,
|
||||
pack_id: String,
|
||||
) -> Result<StylePack, String> {
|
||||
// 安全校验:pack_id 来自远端 backend,可能含路径遍历 segment。
|
||||
// 用跟 read_audio_recording 同样的 UUID-v4 白名单挡住 ../ / 绝对路径等。
|
||||
// backend 当前用 Uuid::new_v4 生成所有 id,合法 id 必然匹配。
|
||||
if !is_valid_session_id(&pack_id) {
|
||||
return Err("invalid pack id".into());
|
||||
}
|
||||
let prefs = coord.prefs().get();
|
||||
let base = marketplace_url_from_prefs(&prefs);
|
||||
let client = reqwest::Client::new();
|
||||
@@ -2417,7 +2426,7 @@ pub async fn marketplace_install(
|
||||
.await
|
||||
.map_err(|e| format!("read body failed: {e}"))?;
|
||||
|
||||
// 写到临时文件后调既有 import_from_zip(复用本地 ZIP 解析逻辑)
|
||||
// pack_id 已经过 UUID 白名单,拼临时文件路径安全。
|
||||
let tmp = std::env::temp_dir().join(format!("openless-marketplace-{pack_id}.zip"));
|
||||
std::fs::write(&tmp, &bytes).map_err(|e| format!("write tmp zip: {e}"))?;
|
||||
let result = coord
|
||||
@@ -2433,6 +2442,10 @@ pub async fn marketplace_upload(
|
||||
coord: CoordinatorState<'_>,
|
||||
pack_id: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
// 本地 style pack id 也是 Uuid::new_v4 字面,跟远端同形态。挡 path traversal。
|
||||
if !is_valid_session_id(&pack_id) {
|
||||
return Err("invalid pack id".into());
|
||||
}
|
||||
let prefs = coord.prefs().get();
|
||||
let base = marketplace_url_from_prefs(&prefs);
|
||||
let dev_user = marketplace_dev_user(&prefs);
|
||||
@@ -2478,6 +2491,9 @@ pub async fn marketplace_like(
|
||||
coord: CoordinatorState<'_>,
|
||||
pack_id: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
if !is_valid_session_id(&pack_id) {
|
||||
return Err("invalid pack id".into());
|
||||
}
|
||||
let prefs = coord.prefs().get();
|
||||
let base = marketplace_url_from_prefs(&prefs);
|
||||
let dev_user = marketplace_dev_user(&prefs);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// 用户在 Settings 填生产 URL 后客户端自动切换。
|
||||
// dev 上传需要 prefs.marketplaceDevLogin(GitHub login 风格)—— 空时上传按钮 disabled。
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '../components/Icon';
|
||||
import {
|
||||
@@ -53,17 +53,23 @@ export function Marketplace() {
|
||||
return () => window.clearTimeout(id);
|
||||
}, [query]);
|
||||
|
||||
// 单调递增 seq 防 stale 响应覆盖:用户快速改 query 时旧请求的 response
|
||||
// 可能晚于新请求到达,比较 seq 丢弃过期结果。
|
||||
const reqSeqRef = useRef(0);
|
||||
const refresh = useCallback(async () => {
|
||||
const seq = ++reqSeqRef.current;
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
try {
|
||||
const list = await listMarketplace({ query: debouncedQuery, sort, limit: 50 });
|
||||
if (seq !== reqSeqRef.current) return; // stale response
|
||||
setItems(list);
|
||||
} catch (error) {
|
||||
if (seq !== reqSeqRef.current) return;
|
||||
console.error('[marketplace] list failed', error);
|
||||
setLoadError(errorMessage(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (seq === reqSeqRef.current) setLoading(false);
|
||||
}
|
||||
}, [debouncedQuery, sort]);
|
||||
|
||||
|
||||
Reference in new issue
Block a user