Усилить проверку источника и ограничение запросов
This commit is contained in:
@@ -29,6 +29,9 @@ CRANK_MASTER_KEY=change-me-master-key
|
|||||||
CRANK_SESSION_SECRET=change-me-session-secret
|
CRANK_SESSION_SECRET=change-me-session-secret
|
||||||
CRANK_PASSWORD_PEPPER=change-me-password-pepper
|
CRANK_PASSWORD_PEPPER=change-me-password-pepper
|
||||||
CRANK_SESSION_TTL_HOURS=24
|
CRANK_SESSION_TTL_HOURS=24
|
||||||
|
# Trust X-Real-IP / X-Forwarded-For for client rate limiting. Enable only when
|
||||||
|
# admin-api runs behind the bundled nginx (or another trusted reverse proxy).
|
||||||
|
CRANK_TRUST_FORWARDED_HEADERS=true
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local
|
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password
|
CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password
|
||||||
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner
|
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner
|
||||||
|
|||||||
Generated
+1
@@ -618,6 +618,7 @@ dependencies = [
|
|||||||
"crank-schema",
|
"crank-schema",
|
||||||
"crank-test-support",
|
"crank-test-support",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
} else {
|
} else {
|
||||||
RequestRateLimiter::new(api_rate_limit)
|
RequestRateLimiter::new(api_rate_limit)
|
||||||
},
|
},
|
||||||
|
trust_forwarded_headers: env_flag("CRANK_TRUST_FORWARDED_HEADERS"),
|
||||||
};
|
};
|
||||||
let app = build_app(state);
|
let app = build_app(state);
|
||||||
let listener = TcpListener::bind(socket_addr).await?;
|
let listener = TcpListener::bind(socket_addr).await?;
|
||||||
|
let make_service = app.into_make_service_with_connect_info::<SocketAddr>();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
runtime_max_concurrent_unary = runtime_limits.max_concurrent_unary,
|
runtime_max_concurrent_unary = runtime_limits.max_concurrent_unary,
|
||||||
@@ -101,7 +103,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
info!("admin-api listening on {}", socket_addr);
|
info!("admin-api listening on {}", socket_addr);
|
||||||
|
|
||||||
axum::serve(listener, app).await?;
|
axum::serve(listener, make_service).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Request, State},
|
extract::{ConnectInfo, Request, State},
|
||||||
http::header::{COOKIE, HeaderMap},
|
http::HeaderMap,
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use crank_runtime::RateLimitRejection;
|
use crank_runtime::RateLimitRejection;
|
||||||
|
|
||||||
use crate::{auth::SESSION_COOKIE_NAME, error::ApiError, state::AppState};
|
use crate::{error::ApiError, state::AppState};
|
||||||
|
|
||||||
pub async fn apply_api_rate_limit(
|
pub async fn apply_api_rate_limit(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
request: Request,
|
request: Request,
|
||||||
next: Next,
|
next: Next,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let key = rate_limit_key(request.headers(), request.uri().path());
|
let peer_ip = request
|
||||||
|
.extensions()
|
||||||
|
.get::<ConnectInfo<SocketAddr>>()
|
||||||
|
.map(|ConnectInfo(address)| address.ip());
|
||||||
|
let key = rate_limit_key(
|
||||||
|
request.headers(),
|
||||||
|
request.uri().path(),
|
||||||
|
peer_ip,
|
||||||
|
state.trust_forwarded_headers,
|
||||||
|
);
|
||||||
if let Err(rejection) = state.api_rate_limiter.check(&key).await {
|
if let Err(rejection) = state.api_rate_limiter.check(&key).await {
|
||||||
return Err(ApiError::rate_limited_with_context(
|
return Err(ApiError::rate_limited_with_context(
|
||||||
"request rate limit exceeded",
|
"request rate limit exceeded",
|
||||||
@@ -30,56 +41,64 @@ fn rejection_context(rejection: RateLimitRejection) -> serde_json::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate_limit_key(headers: &HeaderMap, path: &str) -> String {
|
fn rate_limit_key(
|
||||||
if let Some(session_id) = session_id_from_headers(headers) {
|
headers: &HeaderMap,
|
||||||
return format!("session:{session_id}");
|
path: &str,
|
||||||
|
peer_ip: Option<IpAddr>,
|
||||||
|
trust_forwarded_headers: bool,
|
||||||
|
) -> String {
|
||||||
|
if trust_forwarded_headers && let Some(client_ip) = forwarded_client_ip(headers) {
|
||||||
|
return format!("ip:{client_ip}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(forwarded_for) = header_value(headers, "x-forwarded-for") {
|
if let Some(peer_ip) = peer_ip {
|
||||||
let ip = forwarded_for
|
return format!("ip:{peer_ip}");
|
||||||
.split(',')
|
|
||||||
.next()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.unwrap_or("unknown");
|
|
||||||
return format!("ip:{ip}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(real_ip) = header_value(headers, "x-real-ip") {
|
|
||||||
return format!("ip:{real_ip}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
format!("anonymous:{path}")
|
format!("anonymous:{path}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_id_from_headers(headers: &HeaderMap) -> Option<String> {
|
/// Resolves the client IP from proxy headers, assuming a single trusted proxy.
|
||||||
let cookies = headers.get(COOKIE)?.to_str().ok()?;
|
///
|
||||||
for part in cookies.split(';') {
|
/// `X-Real-IP` is preferred because a trusted proxy (e.g. nginx) sets it to the
|
||||||
let (name, value) = part.trim().split_once('=')?;
|
/// real peer address. For `X-Forwarded-For` the proxy *appends* the observed
|
||||||
if name != SESSION_COOKIE_NAME {
|
/// peer, so the last entry is the trustworthy hop; taking the first entry (as
|
||||||
continue;
|
/// naive implementations do) would let a client spoof its address by sending a
|
||||||
}
|
/// pre-populated header.
|
||||||
let (session_id, _) = value.split_once('.')?;
|
fn forwarded_client_ip(headers: &HeaderMap) -> Option<IpAddr> {
|
||||||
if !session_id.is_empty() {
|
if let Some(real_ip) = header_value(headers, "x-real-ip").and_then(parse_ip) {
|
||||||
return Some(session_id.to_owned());
|
return Some(real_ip);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
header_value(headers, "x-forwarded-for")?
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.rfind(|value| !value.is_empty())
|
||||||
|
.and_then(parse_ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_value<'a>(headers: &'a HeaderMap, name: &'static str) -> Option<&'a str> {
|
fn header_value<'a>(headers: &'a HeaderMap, name: &'static str) -> Option<&'a str> {
|
||||||
headers.get(name)?.to_str().ok().map(str::trim)
|
headers.get(name)?.to_str().ok().map(str::trim)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_ip(value: &str) -> Option<IpAddr> {
|
||||||
|
value.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
use axum::http::{HeaderMap, HeaderValue, header::COOKIE};
|
use axum::http::{HeaderMap, HeaderValue, header::COOKIE};
|
||||||
|
|
||||||
use super::rate_limit_key;
|
use super::rate_limit_key;
|
||||||
|
|
||||||
|
fn peer() -> Option<IpAddr> {
|
||||||
|
Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keys_by_session_cookie_first() {
|
fn unverified_session_cookie_cannot_change_client_key() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
COOKIE,
|
COOKIE,
|
||||||
@@ -88,19 +107,86 @@ mod tests {
|
|||||||
headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.5"));
|
headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.5"));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rate_limit_key(&headers, "/api/auth/login"),
|
rate_limit_key(&headers, "/api/auth/login", peer(), true),
|
||||||
"session:sess_123"
|
"ip:10.0.0.5"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn falls_back_to_forwarded_ip() {
|
fn ignores_forwarded_headers_when_untrusted() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.5"));
|
||||||
|
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.9"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rate_limit_key(&headers, "/api/auth/login", peer(), false),
|
||||||
|
"ip:203.0.113.7"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefers_real_ip_when_trusted() {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
"x-forwarded-for",
|
"x-forwarded-for",
|
||||||
HeaderValue::from_static("10.0.0.5, 10.0.0.6"),
|
HeaderValue::from_static("1.2.3.4, 10.0.0.6"),
|
||||||
|
);
|
||||||
|
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.9"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rate_limit_key(&headers, "/api/auth/login", peer(), true),
|
||||||
|
"ip:10.0.0.9"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uses_last_forwarded_hop_when_trusted() {
|
||||||
|
// A client can prepend spoofed entries; the trusted proxy appends the
|
||||||
|
// real peer, so the last entry is authoritative.
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"x-forwarded-for",
|
||||||
|
HeaderValue::from_static("1.2.3.4, 10.0.0.6"),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(rate_limit_key(&headers, "/api/auth/login"), "ip:10.0.0.5");
|
assert_eq!(
|
||||||
|
rate_limit_key(&headers, "/api/auth/login", peer(), true),
|
||||||
|
"ip:10.0.0.6"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_peer_ip_without_headers() {
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rate_limit_key(&headers, "/api/auth/login", peer(), true),
|
||||||
|
"ip:203.0.113.7"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_invalid_forwarded_ip_values() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("x-real-ip", HeaderValue::from_static("not-an-ip"));
|
||||||
|
headers.insert(
|
||||||
|
"x-forwarded-for",
|
||||||
|
HeaderValue::from_static("198.51.100.8, also-not-an-ip"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rate_limit_key(&headers, "/api/auth/login", peer(), true),
|
||||||
|
"ip:203.0.113.7"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_path_without_peer() {
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rate_limit_key(&headers, "/api/auth/login", None, false),
|
||||||
|
"anonymous:/api/auth/login"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,10 @@ use crank_runtime::RequestRateLimiter;
|
|||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub service: AdminService,
|
pub service: AdminService,
|
||||||
pub api_rate_limiter: RequestRateLimiter,
|
pub api_rate_limiter: RequestRateLimiter,
|
||||||
|
/// Whether to trust `X-Real-IP` / `X-Forwarded-For` for client identification.
|
||||||
|
///
|
||||||
|
/// Only enable when the service sits behind a trusted reverse proxy that
|
||||||
|
/// overwrites these headers (e.g. the bundled nginx). When disabled the
|
||||||
|
/// real TCP peer address is used, which a client cannot spoof.
|
||||||
|
pub trust_forwarded_headers: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,15 +109,19 @@ async fn rejects_rapid_login_requests_with_429() {
|
|||||||
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
|
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
|
||||||
crank_runtime::RequestRateLimitConfig::new(1, 1).unwrap(),
|
crank_runtime::RequestRateLimitConfig::new(1, 1).unwrap(),
|
||||||
),
|
),
|
||||||
|
trust_forwarded_headers: false,
|
||||||
});
|
});
|
||||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
axum::serve(listener, app)
|
axum::serve(
|
||||||
.with_graceful_shutdown(async move {
|
listener,
|
||||||
let _ = shutdown_rx.await;
|
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
|
||||||
})
|
)
|
||||||
.await
|
.with_graceful_shutdown(async move {
|
||||||
.unwrap();
|
let _ = shutdown_rx.await;
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ pub(super) fn build_test_app(
|
|||||||
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
|
api_rate_limiter: crank_runtime::RequestRateLimiter::new(
|
||||||
crank_runtime::RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
|
crank_runtime::RequestRateLimitConfig::new(10_000, 10_000).unwrap(),
|
||||||
),
|
),
|
||||||
|
trust_forwarded_headers: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ crank-registry = { path = "../crank-registry" }
|
|||||||
crank-runtime = { path = "../crank-runtime" }
|
crank-runtime = { path = "../crank-runtime" }
|
||||||
crank-schema = { path = "../crank-schema" }
|
crank-schema = { path = "../crank-schema" }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
reqwest.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use axum::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use futures_util::stream;
|
use futures_util::stream;
|
||||||
|
use reqwest::Url;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::jsonrpc::{
|
use crate::jsonrpc::{
|
||||||
@@ -42,21 +43,44 @@ impl AllowedOrigins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_allowed(&self, origin: &str) -> bool {
|
pub(super) fn is_allowed(&self, origin: &str) -> bool {
|
||||||
if origin.starts_with("http://localhost")
|
let Some(origin) = parse_origin(origin, true) else {
|
||||||
|| origin.starts_with("http://127.0.0.1")
|
return false;
|
||||||
|| origin.starts_with("https://localhost")
|
};
|
||||||
|| origin.starts_with("https://127.0.0.1")
|
|
||||||
{
|
if is_loopback_origin(&origin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
match &self.public_origin {
|
match &self.public_origin {
|
||||||
Some(public_origin) => public_origin == origin,
|
Some(public_origin) => public_origin == &origin.origin().ascii_serialization(),
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_loopback_origin(origin: &Url) -> bool {
|
||||||
|
matches!(origin.host_str(), Some("localhost" | "127.0.0.1" | "[::1]"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_origin(value: &str, origin_only: bool) -> Option<Url> {
|
||||||
|
let origin = Url::parse(value).ok()?;
|
||||||
|
if !matches!(origin.scheme(), "http" | "https")
|
||||||
|
|| origin.host_str().is_none()
|
||||||
|
|| !origin.username().is_empty()
|
||||||
|
|| origin.password().is_some()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if origin_only
|
||||||
|
&& (origin.path() != "/" || origin.query().is_some() || origin.fragment().is_some())
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(origin)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn validate_origin(
|
pub(super) fn validate_origin(
|
||||||
allowed_origins: &AllowedOrigins,
|
allowed_origins: &AllowedOrigins,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
@@ -298,14 +322,58 @@ pub(super) fn is_valid_request_id(value: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn extract_origin(url: &str) -> Option<String> {
|
pub(super) fn extract_origin(url: &str) -> Option<String> {
|
||||||
let mut parts = url.split('/');
|
Some(parse_origin(url, false)?.origin().ascii_serialization())
|
||||||
let scheme = parts.next()?;
|
}
|
||||||
let empty = parts.next()?;
|
|
||||||
let authority = parts.next()?;
|
|
||||||
|
|
||||||
if empty.is_empty() {
|
#[cfg(test)]
|
||||||
return Some(format!("{scheme}//{authority}"));
|
mod tests {
|
||||||
|
use super::AllowedOrigins;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_loopback_origins_with_and_without_port() {
|
||||||
|
let origins = AllowedOrigins::new(None);
|
||||||
|
|
||||||
|
assert!(origins.is_allowed("http://localhost"));
|
||||||
|
assert!(origins.is_allowed("http://localhost:3000"));
|
||||||
|
assert!(origins.is_allowed("https://127.0.0.1:8443"));
|
||||||
|
assert!(origins.is_allowed("http://[::1]:3000"));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
#[test]
|
||||||
|
fn rejects_hosts_that_only_prefix_match_loopback() {
|
||||||
|
let origins = AllowedOrigins::new(None);
|
||||||
|
|
||||||
|
assert!(!origins.is_allowed("http://localhost.attacker.com"));
|
||||||
|
assert!(!origins.is_allowed("http://127.0.0.1.attacker.com"));
|
||||||
|
assert!(!origins.is_allowed("http://localhost@attacker.com"));
|
||||||
|
assert!(!origins.is_allowed("http://attacker.com/http://localhost"));
|
||||||
|
assert!(!origins.is_allowed("http://[::1].attacker.com"));
|
||||||
|
assert!(!origins.is_allowed("http://attacker@[::1]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_configured_public_origin_exactly() {
|
||||||
|
let origins = AllowedOrigins::new(Some("https://crank.example.com".to_owned()));
|
||||||
|
|
||||||
|
assert!(origins.is_allowed("https://crank.example.com"));
|
||||||
|
assert!(!origins.is_allowed("https://crank.example.com.attacker.com"));
|
||||||
|
assert!(!origins.is_allowed("https://evil.example.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_http_schemes() {
|
||||||
|
let origins = AllowedOrigins::new(None);
|
||||||
|
|
||||||
|
assert!(!origins.is_allowed("file://localhost"));
|
||||||
|
assert!(!origins.is_allowed("javascript://localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_values_that_are_not_origins() {
|
||||||
|
let origins = AllowedOrigins::new(None);
|
||||||
|
|
||||||
|
assert!(!origins.is_allowed("http://localhost/path"));
|
||||||
|
assert!(!origins.is_allowed("http://localhost?query=value"));
|
||||||
|
assert!(!origins.is_allowed("http://localhost#fragment"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ CRANK_MASTER_KEY=change-me-master-key
|
|||||||
CRANK_SESSION_SECRET=change-me-session-secret
|
CRANK_SESSION_SECRET=change-me-session-secret
|
||||||
CRANK_PASSWORD_PEPPER=change-me-password-pepper
|
CRANK_PASSWORD_PEPPER=change-me-password-pepper
|
||||||
CRANK_SESSION_TTL_HOURS=24
|
CRANK_SESSION_TTL_HOURS=24
|
||||||
|
# Trust X-Real-IP / X-Forwarded-For for client rate limiting. Keep enabled only
|
||||||
|
# when admin-api runs behind the bundled nginx (or another trusted reverse
|
||||||
|
# proxy). Set to false if you expose admin-api directly to untrusted clients.
|
||||||
|
CRANK_TRUST_FORWARDED_HEADERS=true
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local
|
CRANK_BOOTSTRAP_ADMIN_EMAIL=owner@crank.local
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password
|
CRANK_BOOTSTRAP_ADMIN_PASSWORD=change-me-admin-password
|
||||||
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner
|
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME=Crank Owner
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ services:
|
|||||||
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
|
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
|
||||||
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
|
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
|
||||||
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
|
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
|
||||||
|
CRANK_TRUST_FORWARDED_HEADERS: ${CRANK_TRUST_FORWARDED_HEADERS:-true}
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL: ${CRANK_BOOTSTRAP_ADMIN_EMAIL}
|
CRANK_BOOTSTRAP_ADMIN_EMAIL: ${CRANK_BOOTSTRAP_ADMIN_EMAIL}
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${CRANK_BOOTSTRAP_ADMIN_PASSWORD}
|
CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${CRANK_BOOTSTRAP_ADMIN_PASSWORD}
|
||||||
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME:-Crank Owner}
|
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME:-Crank Owner}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ services:
|
|||||||
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
|
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
|
||||||
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
|
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
|
||||||
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
|
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
|
||||||
|
CRANK_TRUST_FORWARDED_HEADERS: ${CRANK_TRUST_FORWARDED_HEADERS:-true}
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL: ${CRANK_BOOTSTRAP_ADMIN_EMAIL}
|
CRANK_BOOTSTRAP_ADMIN_EMAIL: ${CRANK_BOOTSTRAP_ADMIN_EMAIL}
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${CRANK_BOOTSTRAP_ADMIN_PASSWORD}
|
CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${CRANK_BOOTSTRAP_ADMIN_PASSWORD}
|
||||||
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME:-Crank Owner}
|
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME:-Crank Owner}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ services:
|
|||||||
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
|
CRANK_SESSION_SECRET: ${CRANK_SESSION_SECRET}
|
||||||
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
|
CRANK_PASSWORD_PEPPER: ${CRANK_PASSWORD_PEPPER}
|
||||||
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
|
CRANK_SESSION_TTL_HOURS: ${CRANK_SESSION_TTL_HOURS:-24}
|
||||||
|
CRANK_TRUST_FORWARDED_HEADERS: ${CRANK_TRUST_FORWARDED_HEADERS:-true}
|
||||||
CRANK_BOOTSTRAP_ADMIN_EMAIL: ${CRANK_BOOTSTRAP_ADMIN_EMAIL}
|
CRANK_BOOTSTRAP_ADMIN_EMAIL: ${CRANK_BOOTSTRAP_ADMIN_EMAIL}
|
||||||
CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${CRANK_BOOTSTRAP_ADMIN_PASSWORD}
|
CRANK_BOOTSTRAP_ADMIN_PASSWORD: ${CRANK_BOOTSTRAP_ADMIN_PASSWORD}
|
||||||
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME:-Crank Owner}
|
CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME: ${CRANK_BOOTSTRAP_ADMIN_DISPLAY_NAME:-Crank Owner}
|
||||||
|
|||||||
Reference in New Issue
Block a user