use http::StatusCode;
pub(crate) struct PatternMatch {
pub base_confidence: u8,
pub base_impact: u8,
pub label: Option<&'static str>,
pub leaks: Option<&'static str>,
pub rfc_basis: Option<&'static str>,
}
const NOT_PRESENT: PatternMatch = PatternMatch {
base_confidence: 0, base_impact: 0, label: None, leaks: None, rfc_basis: None,
};
const UNRECOGNISED: PatternMatch = PatternMatch {
base_confidence: 40, base_impact: 15, label: None, leaks: None, rfc_basis: None,
};
const fn pm(
c: u8, i: u8, l: &'static str, lk: &'static str, r: &'static str,
) -> PatternMatch {
PatternMatch {
base_confidence: c, base_impact: i, label: Some(l), leaks: Some(lk), rfc_basis: Some(r),
}
}
pub(crate) fn lookup(baseline: StatusCode, probe: StatusCode) -> PatternMatch {
if baseline == probe { return NOT_PRESENT; }
strong_patterns(baseline, probe)
.or_else(|| upper_moderate(baseline, probe))
.or_else(|| lower_moderate(baseline, probe))
.or_else(|| weak_patterns(baseline, probe))
.unwrap_or(UNRECOGNISED)
}
fn strong_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
use StatusCode as S;
Some(match (b, p) {
(S::OK, S::NOT_FOUND) => pm(92, 50,
"Direct access differential",
"Resource existence confirmed. Response body may contain full representation (IDOR)",
"RFC 9110 \u{00a7}15.3.1"),
(S::PARTIAL_CONTENT, S::NOT_FOUND) => pm(88, 55,
"Range-request differential",
"Resource existence confirmed. Content-Range header may leak resource size",
"RFC 9110 \u{00a7}15.3.7"),
(S::CONFLICT, S::CREATED | S::OK | S::SEE_OTHER | S::ACCEPTED) => pm(86, 45,
"Conflict-based creation differential",
"Resource existence confirmed via uniqueness constraint violation",
"RFC 9110 \u{00a7}15.5.10"),
(S::FORBIDDEN, S::NOT_FOUND) => pm(85, 40,
"Authorization-based differential",
"Resource existence confirmed to low-privilege callers",
"RFC 9110 \u{00a7}15.5.4"),
(S::UNAUTHORIZED, S::NOT_FOUND) => pm(85, 40,
"Authentication-based differential",
"Resource existence confirmed. WWW-Authenticate header leaks auth scheme",
"RFC 9110 \u{00a7}15.5.2"),
_ => return None,
})
}
fn upper_moderate(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
use StatusCode as S;
Some(match (b, p) {
(S::NOT_MODIFIED, S::NOT_FOUND) => pm(84, 40,
"Conditional-request differential",
"Resource existence confirmed via cache validation",
"RFC 9110 \u{00a7}15.4.5"),
(S::UNPROCESSABLE_ENTITY, S::NOT_FOUND) => pm(83, 40,
"Validation-path differential",
"Resource existence confirmed. Validation errors may leak schema",
"RFC 9110 \u{00a7}15.5.21"),
(S::UNPROCESSABLE_ENTITY, S::CREATED) => pm(83, 40,
"Validation-path differential",
"Resource existence confirmed. Server creates nonexistent resources",
"RFC 9110 \u{00a7}9.3.4"),
(S::PRECONDITION_FAILED, S::NOT_FOUND) => pm(83, 40,
"Precondition-failed differential",
"Resource existence confirmed via conditional request evaluation",
"RFC 9110 \u{00a7}13.1.1"),
(S::NOT_ACCEPTABLE, S::NOT_FOUND) => pm(82, 35,
"Content-negotiation differential",
"Resource existence confirmed. Server resolved resource before negotiation",
"RFC 9110 \u{00a7}15.5.7"),
(S::UNSUPPORTED_MEDIA_TYPE, S::NOT_FOUND) => pm(82, 35,
"Media-type differential",
"Resource existence confirmed. Server resolved resource before content-type check",
"RFC 9110 \u{00a7}15.5.16"),
(S::CONFLICT, S::NOT_FOUND) => pm(82, 40,
"State-conflict differential",
"Resource existence confirmed via state constraint violation",
"RFC 9110 \u{00a7}15.5.10"),
(S::CONFLICT, S::NO_CONTENT) => pm(82, 40,
"Conflict-based differential",
"Resource existence confirmed via state conflict against no-content success",
"RFC 9110 \u{00a7}15.5.10"),
(S::RANGE_NOT_SATISFIABLE, S::NOT_FOUND) => pm(82, 50,
"Range-not-satisfiable differential",
"Resource existence confirmed. Content-Range may leak resource size",
"RFC 9110 \u{00a7}15.5.17"),
_ => return None,
})
}
fn lower_moderate(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
use StatusCode as S;
Some(match (b, p) {
(S::GONE, S::NOT_FOUND) => pm(80, 30,
"Tombstone differential",
"Prior resource existence confirmed via tombstone record",
"RFC 9110 \u{00a7}15.5.11"),
(S::NO_CONTENT, S::NOT_FOUND) => pm(82, 35,
"No-content differential",
"Resource existence confirmed with no response body",
"RFC 9110 \u{00a7}9.3.2"),
(S::METHOD_NOT_ALLOWED, S::NOT_FOUND) => pm(82, 35,
"Method-restriction differential",
"Resource existence confirmed. Allow header leaks supported methods",
"RFC 9110 \u{00a7}15.5.6"),
(S::MOVED_PERMANENTLY, S::NOT_FOUND) => pm(80, 30,
"Redirect-based differential",
"Resource existence confirmed via canonical path redirect",
"RFC 9110 \u{00a7}15.4.2"),
(S::PAYLOAD_TOO_LARGE, S::NOT_FOUND) => pm(80, 30,
"Payload-size differential",
"Resource existence confirmed via per-resource size limit",
"RFC 9110 \u{00a7}15.5.14"),
(S::LENGTH_REQUIRED, S::NOT_FOUND) => pm(80, 30,
"Length-required differential",
"Resource existence confirmed. Server resolved resource before length check",
"RFC 9110 \u{00a7}15.5.12"),
(S::ACCEPTED, S::NOT_FOUND) => pm(80, 30,
"Async-acceptance differential",
"Resource existence confirmed via async processing acceptance",
"RFC 9110 \u{00a7}15.3.3"),
(S::INTERNAL_SERVER_ERROR, S::NOT_FOUND) => pm(80, 35,
"Crash-path differential",
"Resource existence confirmed. Server error may leak internals",
"RFC 9110 \u{00a7}15.6.1"),
(S::FOUND, S::NOT_FOUND) => pm(80, 30,
"Temporary-redirect differential",
"Resource existence confirmed via temporary redirect. Location header may leak temporary URI",
"RFC 9110 \u{00a7}15.4.3"),
(S::TEMPORARY_REDIRECT, S::NOT_FOUND) => pm(80, 30,
"Method-preserving temporary-redirect differential",
"Resource existence confirmed via method-preserving temporary redirect",
"RFC 9110 \u{00a7}15.4.8"),
(S::PERMANENT_REDIRECT, S::NOT_FOUND) => pm(80, 30,
"Method-preserving permanent-redirect differential",
"Resource existence confirmed via method-preserving permanent redirect. Location header may leak canonical URI",
"RFC 9110 \u{00a7}15.4.9"),
(S::SEE_OTHER, S::NOT_FOUND) => pm(80, 30,
"Post-mutation redirect differential",
"Resource existence confirmed via post-mutation redirect. Location header may leak result resource URI",
"RFC 9110 \u{00a7}15.4.4"),
_ => return None,
})
}
fn weak_patterns(b: StatusCode, p: StatusCode) -> Option<PatternMatch> {
use StatusCode as S;
Some(match (b, p) {
(S::PAYMENT_REQUIRED, S::NOT_FOUND) => pm(65, 25,
"Payment-gate differential",
"Resource existence confirmed behind paywall",
"RFC 9110 \u{00a7}15.5.3"),
(S::BAD_REQUEST, S::CREATED) => pm(65, 25,
"Client-error creation differential",
"Resource may exist \u{2014} server reached validation before creation",
"RFC 9110 \u{00a7}15.5.1"),
(S::BAD_REQUEST, S::OK) => pm(65, 25,
"Client-error differential",
"Resource may exist \u{2014} server reached validation layer",
"RFC 9110 \u{00a7}15.5.1"),
(S::TOO_MANY_REQUESTS, S::NOT_FOUND) => pm(65, 25,
"Rate-limit-based differential",
"Resource existence confirmed via per-resource rate limiting",
"RFC 6585 \u{00a7}4"),
(S::MULTIPLE_CHOICES, S::NOT_FOUND) => pm(65, 25,
"Multiple-choices differential",
"Resource existence confirmed via content negotiation. Response body may list alternative representations",
"RFC 9110 \u{00a7}15.4.1"),
_ => return None,
})
}