1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Boundary rejection error type with safe HTTP response mapping.
use http::StatusCode;
use thiserror::Error;
/// Errors that may occur when processing a request at the security boundary.
///
/// All variants map to safe HTTP responses. No raw input is ever echoed in the
/// response body — only stable, machine-readable codes are returned.
///
/// This enum is `#[non_exhaustive]` — new variants may be added in future minor versions.
///
/// # Examples
///
/// ```
/// use secure_boundary::error::BoundaryRejection;
///
/// let err = BoundaryRejection::BodyTooLarge;
/// assert_eq!(err.to_string(), "request body too large");
/// ```
#[non_exhaustive]
#[derive(Clone, Debug, Error)]
pub enum BoundaryRejection {
/// The request body exceeded the configured size limit.
#[error("request body too large")]
BodyTooLarge,
/// The `Content-Type` header was missing or not in the allowlist.
#[error("invalid or missing Content-Type")]
InvalidContentType,
/// The request body was malformed or contained unknown fields.
#[error("malformed or unknown-field request body")]
MalformedBody,
/// A path or query parameter failed validation.
#[error("invalid request parameter")]
InvalidParameter,
/// Syntactic validation failed.
#[error("syntactic validation failed")]
SyntaxViolation {
/// A stable internal reason code. Never echoed verbatim to clients.
code: &'static str,
},
/// Semantic validation failed.
#[error("semantic validation failed")]
SemanticViolation {
/// A stable internal reason code. Never echoed verbatim to clients.
code: &'static str,
},
/// The JSON body was nested too deeply.
#[error("request body nesting too deep")]
NestingTooDeep,
/// The JSON body contained too many fields.
#[error("request body has too many fields")]
TooManyFields,
/// A path traversal attempt was detected.
#[error("path traversal detected")]
PathTraversal,
/// An injection attempt was detected (command, SQL, LDAP, filename, redirect).
#[error("injection attempt detected")]
InjectionAttempt {
/// A stable internal reason code. Never echoed verbatim to clients.
code: &'static str,
},
/// An SSRF attempt was blocked (dangerous URL or private IP).
#[error("SSRF attempt blocked")]
SsrfAttempt,
/// An XXE attack was blocked (DOCTYPE or entity expansion in XML).
#[error("XXE attack blocked")]
XxeBlocked,
/// A header value contained CRLF injection characters.
#[error("invalid header value: CRLF detected")]
InvalidHeaderValue,
}
impl BoundaryRejection {
/// Returns the HTTP status code appropriate for this rejection.
#[must_use]
pub fn status_code(&self) -> StatusCode {
match self {
Self::BodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
Self::InvalidContentType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
Self::MalformedBody
| Self::InvalidParameter
| Self::SyntaxViolation { .. }
| Self::SemanticViolation { .. }
| Self::NestingTooDeep
| Self::TooManyFields
| Self::PathTraversal
| Self::InjectionAttempt { .. }
| Self::SsrfAttempt
| Self::XxeBlocked
| Self::InvalidHeaderValue => StatusCode::UNPROCESSABLE_ENTITY,
}
}
/// Returns a stable, client-safe error code string.
///
/// This code is safe to return in HTTP responses — it contains no raw input.
#[must_use]
pub fn client_code(&self) -> &'static str {
match self {
Self::BodyTooLarge => "body_too_large",
Self::InvalidContentType => "invalid_content_type",
Self::MalformedBody => "malformed_body",
Self::InvalidParameter => "invalid_parameter",
Self::SyntaxViolation { .. } => "syntax_violation",
Self::SemanticViolation { .. } => "semantic_violation",
Self::NestingTooDeep => "nesting_too_deep",
Self::TooManyFields => "too_many_fields",
Self::PathTraversal => "path_traversal",
Self::InjectionAttempt { .. } => "injection_attempt",
Self::SsrfAttempt => "ssrf_attempt",
Self::XxeBlocked => "xxe_blocked",
Self::InvalidHeaderValue => "invalid_header_value",
}
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for BoundaryRejection {
fn into_response(self) -> axum::http::Response<axum::body::Body> {
let status = self.status_code();
let code = self.client_code();
// Only stable codes are returned — never raw input
let body = format!(r#"{{"error":{{"code":"{}"}}}}"#, code);
axum::http::Response::builder()
.status(status)
.header("content-type", "application/json")
.body(axum::body::Body::from(body))
.unwrap_or_else(|_| {
axum::http::Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(axum::body::Body::empty())
.expect("static fallback response always builds")
})
}
}