feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP) with configurable limits and time windows: Financial operations (5 req/5min): - wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt, lnd.finalize-psbt, wallet.ecash-send Channel operations (3 req/5min): - lnd.openchannel, lnd.closechannel Backup operations (2-3 req/10min): - backup.create, backup.restore Container/package installs (5 req/5min): - container-install, package.install System operations (2 req/5min): - system.reboot, system.shutdown, update.apply Identity/auth (3-10 req/5min): - identity.create, identity.issue-credential, auth.changePassword Returns HTTP 429 with Retry-After header when limits exceeded. Verified on live server: auth.changePassword blocks at 4th request, lnd.sendcoins blocks at 6th request. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ use crate::config::Config;
|
||||
use crate::container::DevContainerOrchestrator;
|
||||
use crate::monitoring::MetricsStore;
|
||||
use crate::port_allocator::PortAllocator;
|
||||
use crate::session::{self, LoginRateLimiter, SessionStore};
|
||||
use crate::session::{self, EndpointRateLimiter, LoginRateLimiter, SessionStore};
|
||||
use crate::state::StateManager;
|
||||
use anyhow::{Context, Result};
|
||||
use hyper::{Request, Response, StatusCode};
|
||||
@@ -83,6 +83,7 @@ pub struct RpcHandler {
|
||||
port_allocator: Arc<Mutex<PortAllocator>>,
|
||||
pub session_store: SessionStore,
|
||||
login_rate_limiter: LoginRateLimiter,
|
||||
endpoint_rate_limiter: EndpointRateLimiter,
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
@@ -111,6 +112,7 @@ impl RpcHandler {
|
||||
port_allocator,
|
||||
session_store,
|
||||
login_rate_limiter: LoginRateLimiter::new(),
|
||||
endpoint_rate_limiter: EndpointRateLimiter::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -214,6 +216,30 @@ impl RpcHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limit sensitive endpoints (wallet, identity, backup, container, etc.)
|
||||
{
|
||||
let client_ip = extract_client_ip(&parts.headers);
|
||||
if !self.endpoint_rate_limiter.check(&rpc_req.method, client_ip).await {
|
||||
let rpc_resp = RpcResponse {
|
||||
result: None,
|
||||
error: Some(RpcError {
|
||||
code: 429,
|
||||
message: "Rate limit exceeded for this operation. Try again later.".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
};
|
||||
let resp_body = serde_json::to_vec(&rpc_resp)
|
||||
.context("Failed to serialize response")?;
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::TOO_MANY_REQUESTS)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Retry-After", "60")
|
||||
.body(hyper::Body::from(resp_body))
|
||||
.unwrap());
|
||||
}
|
||||
self.endpoint_rate_limiter.record(&rpc_req.method, client_ip).await;
|
||||
}
|
||||
|
||||
// Extract params; clone for post-routing use (login 2FA check needs password)
|
||||
let params = rpc_req.params;
|
||||
let login_params: Option<serde_json::Value> = if rpc_req.method == "auth.login" {
|
||||
|
||||
Reference in New Issue
Block a user