feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
+60 -17
View File
@@ -53,11 +53,7 @@ pub fn parse_sub2api_balance_payload(
return Err("响应格式无效".to_string());
};
if me_payload.get("code").and_then(Value::as_i64).unwrap_or(-1) != 0 {
return Err(me_payload
.get("message")
.and_then(Value::as_str)
.unwrap_or("查询用户信息失败")
.to_string());
return Err("查询用户信息失败".to_string());
}
let Some(me_data) = me_payload.get("data").and_then(Value::as_object) else {
return Err("响应格式无效".to_string());
@@ -126,11 +122,12 @@ pub fn attach_balance_checkin_outcome(
.entry("extra".to_string())
.or_insert_with(|| Value::Object(Map::new()));
if let Some(extra) = extra.as_object_mut() {
let message = stable_checkin_outcome_message(outcome);
if outcome.cookie_expired {
extra.insert("cookie_expired".to_string(), Value::Bool(true));
extra.insert(
"cookie_expired_message".to_string(),
Value::String(outcome.message.clone()),
Value::String(message.to_string()),
);
} else {
extra.insert(
@@ -139,7 +136,7 @@ pub fn attach_balance_checkin_outcome(
);
extra.insert(
"checkin_message".to_string(),
Value::String(outcome.message.clone()),
Value::String(message.to_string()),
);
}
}
@@ -151,6 +148,17 @@ pub fn attach_balance_checkin_outcome(
}
}
fn stable_checkin_outcome_message(outcome: &ProviderOpsCheckinOutcome) -> &'static str {
if outcome.cookie_expired {
return "Cookie 已失效";
}
match outcome.success {
Some(true) => "签到成功",
Some(false) => "签到失败",
None => "今日已签到",
}
}
pub fn build_balance_data(
total_granted: Option<f64>,
total_used: Option<f64>,
@@ -177,11 +185,7 @@ fn parse_new_api_balance_payload(
{
response_json.get("data")
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
return Err(response_json
.get("message")
.and_then(Value::as_str)
.unwrap_or("业务状态码表示失败")
.to_string());
return Err("业务状态码表示失败".to_string());
} else {
Some(response_json)
};
@@ -265,11 +269,7 @@ fn parse_cubence_balance_payload(
{
response_json.get("data")
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
return Err(response_json
.get("message")
.and_then(Value::as_str)
.unwrap_or("查询余额失败")
.to_string());
return Err("查询余额失败".to_string());
} else {
Some(response_json)
};
@@ -694,4 +694,47 @@ mod tests {
assert_eq!(payload["status"], json!("auth_expired"));
assert_eq!(payload["data"]["extra"]["cookie_expired"], json!(true));
}
#[test]
fn attach_balance_checkin_outcome_does_not_copy_upstream_message() {
let mut payload = json!({
"status": "success",
"data": { "extra": {} }
});
attach_balance_checkin_outcome(
&mut payload,
&ProviderOpsCheckinOutcome {
success: Some(true),
message: "authorization=Bearer upstream-secret".to_string(),
cookie_expired: false,
},
);
assert_eq!(
payload["data"]["extra"]["checkin_message"],
json!("签到成功")
);
assert!(!payload.to_string().contains("upstream-secret"));
}
#[test]
fn balance_parsers_do_not_return_upstream_error_messages() {
let config = json!({}).as_object().cloned().expect("config");
let secret = "authorization=Bearer upstream-secret";
let generic_error = parse_query_balance_payload(
"generic_api",
&config,
&json!({"success": false, "message": secret}),
)
.expect_err("generic API failure should be rejected");
let sub2api_error =
parse_sub2api_balance_payload(&config, &json!({"code": 401, "message": secret}), None)
.expect_err("Sub2API failure should be rejected");
assert_eq!(generic_error, "业务状态码表示失败");
assert_eq!(sub2api_error, "查询用户信息失败");
assert!(!generic_error.contains("upstream-secret"));
assert!(!sub2api_error.contains("upstream-secret"));
}
}
+84 -35
View File
@@ -5,6 +5,7 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::{json, Map, Value};
const ADMIN_PROVIDER_OPS_ANYROUTER_XOR_KEY: &str = "3000176000856006061501533003690027800375";
const ADMIN_PROVIDER_OPS_ANYROUTER_SESSION_PART_MAX_BYTES: usize = 256 * 1024;
const ADMIN_PROVIDER_OPS_ANYROUTER_UNSBOX_TABLE: [usize; 40] = [
0xF, 0x23, 0x1D, 0x18, 0x21, 0x10, 0x1, 0x26, 0xA, 0x9, 0x13, 0x1F, 0x28, 0x1B, 0x16, 0x17,
0x19, 0xD, 0x6, 0xB, 0x27, 0x12, 0x14, 0x8, 0xE, 0x15, 0x20, 0x1A, 0x2, 0x1E, 0x7, 0x4, 0x11,
@@ -186,7 +187,16 @@ pub fn admin_provider_ops_anyrouter_parse_session_user_id(cookie_input: &str) ->
}
fn decode_python_urlsafe_b64(input: &str) -> Option<Vec<u8>> {
let normalized = input.trim().replace('-', "+").replace('_', "/");
let input = input.trim();
let max_encoded_len = ADMIN_PROVIDER_OPS_ANYROUTER_SESSION_PART_MAX_BYTES
.saturating_add(2)
.checked_div(3)
.unwrap_or(usize::MAX)
.saturating_mul(4);
if input.is_empty() || input.len() > max_encoded_len {
return None;
}
let normalized = input.replace('-', "+").replace('_', "/");
if normalized.is_empty() {
return None;
}
@@ -195,7 +205,8 @@ fn decode_python_urlsafe_b64(input: &str) -> Option<Vec<u8>> {
if remainder != 0 {
padded.push_str(&"=".repeat(4 - remainder));
}
STANDARD.decode(padded.as_bytes()).ok()
let decoded = STANDARD.decode(padded.as_bytes()).ok()?;
(decoded.len() <= ADMIN_PROVIDER_OPS_ANYROUTER_SESSION_PART_MAX_BYTES).then_some(decoded)
}
pub fn admin_provider_ops_verify_failure(message: impl Into<String>) -> Value {
@@ -618,12 +629,7 @@ fn verify_payload_with_auth_messages(
{
response_json.get("data")
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
return admin_provider_ops_verify_failure(
response_json
.get("message")
.and_then(Value::as_str)
.unwrap_or("验证失败"),
);
return admin_provider_ops_verify_failure("验证失败");
} else {
Some(response_json)
};
@@ -632,17 +638,6 @@ fn verify_payload_with_auth_messages(
return admin_provider_ops_verify_failure("响应格式无效");
};
let mut extra = Map::new();
for (key, value) in user_data {
if matches!(
key.as_str(),
"username" | "display_name" | "email" | "quota" | "used_quota" | "request_count"
) {
continue;
}
extra.insert(key.clone(), value.clone());
}
admin_provider_ops_verify_success(
admin_provider_ops_verify_user_payload_with_usage(
user_data
@@ -660,7 +655,7 @@ fn verify_payload_with_auth_messages(
admin_provider_ops_value_as_f64(user_data.get("quota")),
admin_provider_ops_value_as_f64(user_data.get("used_quota")),
admin_provider_ops_value_as_u64(user_data.get("request_count")),
Some(extra),
None,
),
None,
)
@@ -685,12 +680,7 @@ pub fn admin_provider_ops_cubence_verify_payload(
{
response_json.get("data")
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
return admin_provider_ops_verify_failure(
response_json
.get("message")
.and_then(Value::as_str)
.unwrap_or("验证失败"),
);
return admin_provider_ops_verify_failure("验证失败");
} else {
Some(response_json)
};
@@ -864,12 +854,7 @@ pub fn admin_provider_ops_sub2api_verify_payload(
return admin_provider_ops_verify_failure("响应格式无效");
};
if payload.get("code").and_then(Value::as_i64).unwrap_or(-1) != 0 {
return admin_provider_ops_verify_failure(
payload
.get("message")
.and_then(Value::as_str)
.unwrap_or("验证失败"),
);
return admin_provider_ops_verify_failure("验证失败");
}
let Some(user_data) = payload.get("data").and_then(Value::as_object) else {
@@ -915,9 +900,10 @@ mod tests {
admin_provider_ops_anyrouter_compute_acw_sc_v2,
admin_provider_ops_anyrouter_parse_session_user_id,
admin_provider_ops_anyrouter_verify_payload, admin_provider_ops_cubence_verify_payload,
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_sub2api_verify_payload,
admin_provider_ops_usage_api_verify_payload, admin_provider_ops_verify_headers,
parse_verify_payload, ADMIN_PROVIDER_OPS_USER_AGENT,
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_generic_verify_payload,
admin_provider_ops_sub2api_verify_payload, admin_provider_ops_usage_api_verify_payload,
admin_provider_ops_verify_headers, parse_verify_payload,
ADMIN_PROVIDER_OPS_ANYROUTER_SESSION_PART_MAX_BYTES, ADMIN_PROVIDER_OPS_USER_AGENT,
};
use http::StatusCode;
use reqwest::header::COOKIE;
@@ -951,6 +937,17 @@ mod tests {
assert_eq!(actual.as_deref(), Some("42"));
}
#[test]
fn anyrouter_parse_session_user_id_rejects_oversized_encoded_cookie() {
let encoded_limit = ADMIN_PROVIDER_OPS_ANYROUTER_SESSION_PART_MAX_BYTES
.saturating_add(2)
.checked_div(3)
.unwrap()
.saturating_mul(4);
let cookie = format!("session={}", "A".repeat(encoded_limit + 1));
assert!(admin_provider_ops_anyrouter_parse_session_user_id(&cookie).is_none());
}
#[test]
fn frontend_updated_credentials_omits_internal_runtime_fields() {
let filtered = admin_provider_ops_frontend_updated_credentials(Map::from_iter([
@@ -1102,6 +1099,58 @@ mod tests {
assert_eq!(auth_failed["message"], json!("Cookie 已失效,请重新配置"));
}
#[test]
fn generic_verify_payload_does_not_reflect_upstream_errors_or_unknown_fields() {
let failed = admin_provider_ops_generic_verify_payload(
StatusCode::OK,
&json!({
"success": false,
"message": "token=upstream-secret https://user:[email protected]/path?q=secret"
}),
);
assert_eq!(failed["success"], json!(false));
assert_eq!(failed["message"], json!("验证失败"));
let succeeded = admin_provider_ops_generic_verify_payload(
StatusCode::OK,
&json!({
"username": "alice",
"quota": 12.5,
"access_token": "upstream-secret",
"profile": {"private_note": "sensitive"}
}),
);
assert_eq!(succeeded["success"], json!(true));
assert_eq!(succeeded["data"]["username"], json!("alice"));
assert_eq!(succeeded["data"]["quota"], json!(12.5));
assert_eq!(succeeded["data"]["extra"], json!({}));
let serialized = succeeded.to_string();
assert!(!serialized.contains("upstream-secret"));
assert!(!serialized.contains("private_note"));
}
#[test]
fn architecture_verify_failures_do_not_reflect_upstream_messages() {
let cubence = admin_provider_ops_cubence_verify_payload(
StatusCode::OK,
&json!({
"success": false,
"message": "authorization=Bearer upstream-secret"
}),
);
assert_eq!(cubence["message"], json!("验证失败"));
let sub2api = admin_provider_ops_sub2api_verify_payload(
StatusCode::OK,
&json!({
"code": 500,
"message": "cookie=session-secret"
}),
None,
);
assert_eq!(sub2api["message"], json!("验证失败"));
}
#[test]
fn done_hub_verify_payload_reads_wrapped_profile() {
let payload = parse_verify_payload(