Api Key Exposure in Axum
How Api Key Exposure Manifests in Axum
API key exposure in Axum applications typically occurs through several Rust-specific patterns that developers might overlook. The most common scenario involves hardcoding API keys directly in source code or configuration files that get committed to version control. In Axum, this often appears in route handlers where keys are passed directly to external services:
use axum::{routing::get, Router};
async fn fetch_data() -> String {
let api_key = "sk-1234567890"; // EXPOSED IN SOURCE
let client = reqwest::Client::new();
let res = client
.get("https://api.example.com/data")
.header("Authorization", format!("Bearer {}", api_key))
.send()
.await;
res.unwrap().text().await
}
let app = Router::new().route("/data", get(fetch_data));
Another Axum-specific manifestation occurs when using extractors for authentication. Developers sometimes create custom extractors that inadvertently log or expose sensitive credentials:
use axum::extract::FromRequest;
struct ApiKey(String);
#[async_trait]
impl FromRequest for ApiKey
where
B: Send,
{
type Rejection = ();
async fn from_request(req: &mut axum::RequestParts) -> Result<Self, Self::Rejection> {
let headers = req.headers().clone(); // POTENTIAL EXPOSURE
let api_key = headers.get("x-api-key").cloned();
Ok(ApiKey(api_key.unwrap().to_string()))
}
}
Environment variable handling in Axum applications can also lead to exposure if keys are logged or included in error responses. The async nature of Axum means that error handling across await points can inadvertently capture and expose sensitive data in debug logs:
#[tokio::main]
async fn main() {
let api_key = std::env::var("API_KEY").unwrap();
let app = Router::new()
.route("/health", get(|| async {
tracing::info!("API Key: {}", api_key); // LOGGING EXPOSURE
"healthy"
}));
axum::Server::bind(&([127, 0, 0, 1], 3000)).serve(app.into_make_service()).await.unwrap();
}
Axum-Specific Detection
Detecting API key exposure in Axum applications requires both static analysis and runtime scanning. Static analysis should focus on common Rust patterns where keys appear:
std::env::var()calls that aren't wrapped in error handling- Hardcoded strings that match API key patterns
- Custom extractors that clone or log headers
- Error responses that might contain sensitive data
For runtime detection, middleBrick's black-box scanning approach is particularly effective for Axum applications. The scanner tests unauthenticated endpoints and analyzes responses for exposed credentials:
# Install middleBrick CLI
npm install -g middlebrick
# Scan your Axum API
middlebrick scan https://your-axum-api.com
# Or integrate into CI/CD
middlebrick scan --output json --threshold B
middleBrick specifically checks for API key exposure patterns including:
- Authorization headers in responses
- Environment variable names in error messages
- Debug information containing credentials
- Default configuration files with embedded keys
The scanner's 12 security checks include Input Validation and Data Exposure modules that flag when Axum applications inadvertently return sensitive information. For LLM/AI security, middleBrick also tests whether Axum APIs that integrate with language models might expose system prompts or training data.
Axum-Specific Remediation
Remediating API key exposure in Axum requires leveraging Rust's type system and Axum's middleware architecture. The first step is implementing proper configuration management using the config crate:
use config::{Config, Environment};
#[derive(Debug, Deserialize)]
struct Settings {
api_key: String,
}
async fn fetch_data(settings: Settings) -> String {
let client = reqwest::Client::new();
let res = client
.get("https://api.example.com/data")
.header("Authorization", format!("Bearer {}", settings.api_key))
.send()
.await;
res.unwrap().text().await
}
#[tokio::main]
async fn main() -> Result<(), config::ConfigError> {
let mut settings = Config::builder()
.add_source(Environment::default())
.build()?;
let settings: Settings = settings.try_deserialize()?;
let app = Router::new()
.route("/data", get(|| async { fetch_data(settings.clone()).await }));
axum::Server::bind(&([127, 0, 0, 1], 3000)).serve(app.into_make_service()).await.unwrap();
Ok(())
}
For authentication extractors, implement secure key handling without cloning headers:
use axum::extract::FromRequest;
use http::request::Parts;
struct ApiKey(String);
#[async_trait]
impl FromRequest for ApiKey
where
B: Send,
{
type Rejection = ();
async fn from_request(req: &mut axum::RequestParts) -> Result<Self, Self::Rejection> {
let headers = req.headers().ok_or(())?;
let api_key = headers.get("x-api-key").cloned().ok_or(())?;
Ok(ApiKey(api_key.to_string()))
}
}
Implement middleware to prevent accidental exposure in error responses:
use axum::middleware::Next;
use axum::response::IntoResponse;
async fn sanitize_error(mut req: axum::Request, next: Next) -> axum::response::Response {
let res = next.run(req).await;
if let Err(err) = res.response() {
// Remove sensitive headers from error responses
let sanitized = err.into_response();
return sanitized.map(axum::body::boxed);
}
res
}
let app = Router::new()
.route("/data", get(fetch_data))
.layer(axum::middleware::from_fn(sanitize_error));