Files
yvgude--lean-ctx/rust/tests/security_hardening.rs
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

405 lines
14 KiB
Rust

//! Security hardening tests — validates all Critical and High fixes from the
//! bank-readiness audit (2026-05-08).
/// Reads the full `LeanCtxServer` dispatch source across its split submodules
/// (`mod.rs`, `call_tool.rs`, `server_handler.rs`, plus the extracted
/// `post_process.rs` / `post_dispatch.rs` stages) so that the security-invariant
/// checks below stay robust to internal module structure.
fn server_dispatch_src() -> String {
format!(
"{}\n{}\n{}\n{}\n{}",
include_str!("../src/server/mod.rs"),
include_str!("../src/server/call_tool.rs"),
include_str!("../src/server/server_handler.rs"),
include_str!("../src/server/post_process.rs"),
include_str!("../src/server/post_dispatch.rs"),
)
}
// ---------------------------------------------------------------------------
// C1 — Dashboard: token not leaked without valid ?token= query
// ---------------------------------------------------------------------------
#[test]
fn dashboard_route_response_omits_token_without_valid_query() {
let src = include_str!("../src/dashboard/routes/mod.rs");
assert!(
src.contains("is_some_and(|q| super::constant_time_eq(q.as_bytes(), expected.as_bytes()))"),
"C1: dashboard must use constant_time_eq via is_some_and for query token validation"
);
assert!(
src.contains("if valid_query"),
"C1: token embedding gated on valid_query (no loopback bypass)"
);
assert!(
!src.contains("if valid_query || is_loopback"),
"C1: loopback must NOT bypass token validation — removed for security"
);
assert!(
src.contains("_is_loopback: bool"),
"C1: route_response signature accepts _is_loopback (unused, kept for API compat)"
);
}
#[test]
fn dashboard_api_auth_never_bypassed_for_loopback() {
let src = include_str!("../src/dashboard/mod.rs");
assert!(
!src.contains("if requires_auth && !has_header_auth && !is_loopback"),
"C1: API auth must NOT be bypassed for loopback — only HTML token injection is allowed"
);
assert!(
src.contains("if requires_auth && !has_header_auth"),
"C1: API auth gate must remain unconditional (no loopback exception)"
);
}
#[test]
fn dashboard_check_auth_uses_constant_time_eq() {
let src = include_str!("../src/dashboard/mod.rs");
assert!(
src.contains("constant_time_eq(token.trim().as_bytes(), expected_token.as_bytes())"),
"C1: check_auth must use constant_time_eq, not plain =="
);
}
#[test]
fn dashboard_probe_sends_bearer_token() {
let src = include_str!("../src/dashboard/mod.rs");
assert!(
src.contains("Authorization: Bearer {t}"),
"C1: dashboard_responding probe must send saved Bearer token"
);
}
#[test]
fn dashboard_metrics_requires_auth() {
let src = include_str!("../src/dashboard/mod.rs");
assert!(
src.contains(r#"path == "/metrics""#),
"C1: /metrics must be in the requires_auth path"
);
}
// ---------------------------------------------------------------------------
// C2 — Team server: workspace enforced from TeamRequestContext
// ---------------------------------------------------------------------------
#[test]
fn context_views_use_resolve_workspace() {
let src = include_str!("../src/http_server/context_views.rs");
assert!(
src.contains("fn resolve_workspace"),
"C2: context_views must have a resolve_workspace helper"
);
let search_fn = src
.find("v1_events_search")
.expect("v1_events_search missing");
let search_body = &src[search_fn..search_fn + 600];
assert!(
search_body.contains("resolve_workspace("),
"C2: v1_events_search must call resolve_workspace"
);
}
#[test]
fn lineage_filters_by_workspace() {
let src = include_str!("../src/core/context_os/context_bus.rs");
let lineage_fn = src.find("fn lineage(").expect("lineage fn missing");
let lineage_sig = &src[lineage_fn..lineage_fn + 200];
assert!(
lineage_sig.contains("workspace_id: &str"),
"C2: lineage() must take workspace_id parameter"
);
let lineage_body = &src[lineage_fn..lineage_fn + 800];
assert!(
lineage_body.contains("AND workspace_id = ?2"),
"C2: lineage SQL must filter by workspace_id"
);
}
// ---------------------------------------------------------------------------
// H1 — Shell CWD jail enforcement
// ---------------------------------------------------------------------------
#[test]
fn effective_cwd_calls_jail() {
let src = include_str!("../src/core/session/state.rs");
// `effective_cwd` must not bypass the jail: it delegates to the checked
// variant, which is the single enforcement point (#629/#633 refactor).
let ecwd_fn = src
.find("fn effective_cwd(")
.expect("effective_cwd missing");
let ecwd_body = &src[ecwd_fn..ecwd_fn + 500];
assert!(
ecwd_body.contains("effective_cwd_checked("),
"H1: effective_cwd must delegate to effective_cwd_checked"
);
// The checked variant enforces the project-root jail for an explicit cwd.
let checked_fn = src
.find("fn effective_cwd_checked(")
.expect("effective_cwd_checked missing");
let checked_body = &src[checked_fn..checked_fn + 500];
assert!(
checked_body.contains("jail_cwd(cwd, root)"),
"H1: effective_cwd_checked must call jail_cwd for explicit cwd"
);
}
#[test]
fn update_shell_cwd_calls_jail_path() {
let src = include_str!("../src/core/session/state.rs");
let uscwd_fn = src
.find("fn update_shell_cwd(")
.expect("update_shell_cwd missing");
let uscwd_body = &src[uscwd_fn..uscwd_fn + 600];
assert!(
uscwd_body.contains("jail_path("),
"H1: update_shell_cwd must jail_path check before storing"
);
}
// ---------------------------------------------------------------------------
// H2 — MCP ctx_read path has secret check
// ---------------------------------------------------------------------------
#[test]
fn resolve_path_includes_secret_check() {
let src = include_str!("../src/tools/server_paths.rs");
let resolve_fn = src.find("fn resolve_path(").expect("resolve_path missing");
let resolve_body = &src[resolve_fn..];
let end = resolve_body
.find("fn resolve_path_or_passthrough")
.unwrap_or(resolve_body.len());
let resolve_body = &resolve_body[..end];
assert!(
resolve_body.contains("check_secret_path_for_tool"),
"H2: resolve_path must call check_secret_path_for_tool"
);
}
// ---------------------------------------------------------------------------
// H3 — REST event responses are redacted
// ---------------------------------------------------------------------------
#[test]
fn event_search_applies_redaction() {
let src = include_str!("../src/http_server/context_views.rs");
let search_fn = src
.find("v1_events_search")
.expect("v1_events_search missing");
let search_body = &src[search_fn..search_fn + 800];
assert!(
search_body.contains("redact_event_payload("),
"H3: v1_events_search must redact event payloads"
);
}
#[test]
fn event_lineage_applies_redaction() {
let src = include_str!("../src/http_server/context_views.rs");
let lineage_fn = src
.find("v1_event_lineage")
.expect("v1_event_lineage missing");
let lineage_body = &src[lineage_fn..lineage_fn + 800];
assert!(
lineage_body.contains("redact_event_payload("),
"H3: v1_event_lineage must redact event payloads"
);
}
// ---------------------------------------------------------------------------
// H4 — JSON-RPC batch scope bypass prevention
// ---------------------------------------------------------------------------
#[test]
fn team_auth_rejects_batch_requests() {
let src = include_str!("../src/http_server/team/mod.rs");
assert!(
src.contains("batch_requests_not_supported"),
"H4: team auth must reject JSON-RPC batch (array) requests"
);
assert!(
src.contains("let mut allow = false;"),
"H4: team auth must default allow to false"
);
}
// ---------------------------------------------------------------------------
// H5 — npm postinstall SHA256 verification
// ---------------------------------------------------------------------------
#[test]
fn postinstall_has_sha256_verification() {
let src = include_str!("../../packages/lean-ctx-bin/postinstall.js");
assert!(
src.contains("createHash(\"sha256\")") || src.contains("createHash('sha256')"),
"H5: postinstall.js must compute SHA256 hash"
);
assert!(
src.contains("SHA256SUMS"),
"H5: postinstall.js must download SHA256SUMS for verification"
);
assert!(
src.contains("SHA256 mismatch"),
"H5: postinstall.js must abort on hash mismatch"
);
}
// ---------------------------------------------------------------------------
// H6 — Pipeline archive redaction
// ---------------------------------------------------------------------------
#[test]
fn pipeline_archive_uses_redacted_output() {
let src = crate::server_dispatch_src();
assert!(
src.contains("redact_text_if_enabled"),
"H6: shell archive must use redact_text_if_enabled before storing"
);
}
// ---------------------------------------------------------------------------
// M2 — ReDoS guard in ctx_search
// ---------------------------------------------------------------------------
#[test]
fn ctx_search_has_pattern_length_limit() {
let src = include_str!("../src/tools/ctx_search.rs");
assert!(
src.contains("MAX_PATTERN_LEN"),
"M2: ctx_search must limit pattern length"
);
assert!(
src.contains(".size_limit("),
"M2: ctx_search must set regex size_limit"
);
assert!(
src.contains("RegexBuilder::new("),
"M2: ctx_search must use RegexBuilder (not Regex::new) for size limits"
);
}
// ---------------------------------------------------------------------------
// M3 — MCP stdio max_length
// ---------------------------------------------------------------------------
#[test]
fn mcp_stdio_has_bounded_max_length() {
let src = include_str!("../src/mcp_stdio.rs");
assert!(
!src.contains("max_length: usize::MAX"),
"M3: MCP stdio must NOT use usize::MAX for max_length"
);
assert!(
src.contains("32 * 1024 * 1024"),
"M3: MCP stdio max_length must be 32 MiB"
);
}
// ---------------------------------------------------------------------------
// M5 — UDS socket permissions (now in ipc/unix.rs)
// ---------------------------------------------------------------------------
#[test]
fn uds_socket_sets_permissions() {
let src = include_str!("../src/ipc/unix.rs");
assert!(
src.contains("PermissionsExt"),
"M5: ipc/unix.rs must import PermissionsExt"
);
assert!(
src.contains("0o600"),
"M5: ipc/unix.rs must set socket permissions to 0o600"
);
}
// ---------------------------------------------------------------------------
// L1 — Error responses sanitized
// ---------------------------------------------------------------------------
#[test]
fn http_server_sanitizes_error_responses() {
let src = include_str!("../src/http_server/mod.rs");
let v1_fn = src.find("v1_tool_call").expect("v1_tool_call missing");
let v1_body = &src[v1_fn..v1_fn + 600];
assert!(
!v1_body.contains("e.to_string()"),
"L1: v1_tool_call must not return internal error details"
);
}
// ---------------------------------------------------------------------------
// CSP nonce injection for ALL inline scripts
// ---------------------------------------------------------------------------
#[test]
fn csp_nonce_covers_all_inline_scripts() {
let src = include_str!("../src/dashboard/mod.rs");
assert!(
src.contains("add_nonce_to_inline_scripts"),
"dashboard must use add_nonce_to_inline_scripts for all inline scripts"
);
assert!(
!src.contains(r"<script>window.__LEAN_CTX_TOKEN__")
|| src.contains("add_nonce_to_inline_scripts"),
"token script nonce must go through add_nonce_to_inline_scripts, not ad-hoc replace"
);
}
#[test]
fn add_nonce_skips_external_scripts() {
let html = r#"<script src="foo.js"></script><script>inline()</script><script type="module">boot()</script>"#;
let result = lean_ctx::dashboard::add_nonce_to_inline_scripts(html, "abc123");
assert!(
result.contains(r#"<script src="foo.js">"#),
"external script must NOT get nonce: {result}"
);
assert!(
result.contains(r#"<script nonce="abc123">inline()"#),
"inline script must get nonce: {result}"
);
assert!(
result.contains(r#"<script nonce="abc123" type="module">boot()"#),
"inline module must get nonce: {result}"
);
}
// ---------------------------------------------------------------------------
// raw=true bypasses ALL post-processing in call_tool
// ---------------------------------------------------------------------------
#[test]
fn raw_shell_skips_all_postprocessing() {
let src = crate::server_dispatch_src().replace("\r\n", "\n");
assert!(
src.contains("let is_raw_shell = name == \"ctx_shell\""),
"call_tool must compute is_raw_shell flag"
);
assert!(
src.contains("if minimal || is_raw_shell {"),
"archive_hint must be skipped for raw shell"
);
assert!(
src.contains("is_raw_shell")
&& src.contains("tool_saved_tokens > 0")
&& src.contains("skip_terse"),
"skip_terse must include is_raw_shell and double-compression guard"
);
// The verify_output guard may AND extra predicates after !is_raw_shell
// (e.g. !firewalled, verify_footer()), so accept both the bare guard and
// the extended form rather than pinning a brittle exact literal.
assert!(
(src.contains("if !is_raw_shell {") || src.contains("if !is_raw_shell &&"))
&& src.contains("verify_output"),
"output verification must be skipped for raw shell"
);
assert!(
src.contains("!is_raw_shell && name == \"ctx_shell\""),
"shell_efficiency_hint must be skipped for raw shell"
);
}