Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use std::env;

/// Central application configuration, loaded once at startup from environment variables.
Expand Down Expand Up @@ -152,6 +152,12 @@ impl Config {
_ => AppEnv::Development,
};

if app_env == AppEnv::Production
&& (allowed_origins.is_empty() || allowed_origins.iter().any(|o| o == "*"))
{
bail!("ALLOWED_ORIGINS must be set to an explicit, non-wildcard list of origins in production");
}

Ok(Self {
port,
host,
Expand Down
41 changes: 38 additions & 3 deletions src/middleware/cors.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use axum::http::{header, Method};
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};

/// Build the CORS layer from the configured list of allowed origins.
///
/// When `allowed_origins` contains `"*"`, or is empty, every origin is
/// permitted (suitable for development / public APIs). In production, supply
/// permitted (suitable for development / public APIs). In production, supply
/// an explicit list so that only known origins are whitelisted.
///
/// Methods and headers are restricted to the real surface required by the
/// StellarSend API (GET, POST, OPTIONS, and standard auth/content headers)
/// with a 1-hour preflight cache max age.
pub fn build_cors_layer(allowed_origins: &[String]) -> CorsLayer {
let allow_origin: AllowOrigin = if allowed_origins.iter().any(|o| o == "*")
|| allowed_origins.is_empty()
Expand All @@ -20,6 +25,36 @@ pub fn build_cors_layer(allowed_origins: &[String]) -> CorsLayer {

CorsLayer::new()
.allow_origin(allow_origin)
.allow_methods(AllowMethods::any())
.allow_headers(AllowHeaders::any())
.allow_methods(AllowMethods::list([
Method::GET,
Method::POST,
Method::OPTIONS,
]))
.allow_headers(AllowHeaders::list([
header::AUTHORIZATION,
header::CONTENT_TYPE,
header::ACCEPT,
header::HeaderName::from_static("x-request-id"),
]))
.max_age(std::time::Duration::from_secs(3600))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn build_cors_layer_with_wildcard_works() {
let origins = vec!["*".to_string()];
let _layer = build_cors_layer(&origins);
}

#[test]
fn build_cors_layer_with_explicit_origins_works() {
let origins = vec![
"https://app.stellarsend.com".to_string(),
"https://staging.stellarsend.com".to_string(),
];
let _layer = build_cors_layer(&origins);
}
}