harden federation: sanitize incoming AP content, fix error handling, tighten rate limits
Some checks failed
CI / Check / Test (push) Successful in 10m54s
CI / Release build (push) Failing after 1m20s

This commit is contained in:
2026-05-29 12:23:29 +02:00
parent d1f9f55d4f
commit 84ddf04d28
7 changed files with 188 additions and 9 deletions

167
Cargo.lock generated
View File

@@ -6,6 +6,7 @@ version = 4
name = "activitypub" name = "activitypub"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"ammonia",
"anyhow", "anyhow",
"async-trait", "async-trait",
"axum", "axum",
@@ -260,6 +261,19 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
version = "4.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
dependencies = [
"cssparser",
"html5ever",
"maplit",
"tendril",
"url",
]
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -1236,6 +1250,29 @@ dependencies = [
"phf", "phf",
] ]
[[package]]
name = "cssparser"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa"
dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf",
"smallvec",
]
[[package]]
name = "cssparser-macros"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.4.0" version = "1.4.0"
@@ -1550,6 +1587,21 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dtoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
[[package]]
name = "dtoa-short"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
dependencies = [
"dtoa",
]
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.5" version = "1.0.5"
@@ -1903,6 +1955,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.32" version = "0.3.32"
@@ -2205,6 +2267,17 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "html5ever"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4"
dependencies = [
"log",
"markup5ever",
"match_token",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@@ -2965,6 +3038,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mac_address" name = "mac_address"
version = "1.1.8" version = "1.1.8"
@@ -2975,6 +3054,34 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3"
dependencies = [
"log",
"tendril",
"web_atoms",
]
[[package]]
name = "match_token"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.2.0" version = "0.2.0"
@@ -3791,6 +3898,12 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]] [[package]]
name = "presentation" name = "presentation"
version = "0.1.0" version = "0.1.0"
@@ -5245,6 +5358,31 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string_cache"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.5" version = "0.1.5"
@@ -5401,6 +5539,17 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]] [[package]]
name = "terminfo" name = "terminfo"
version = "0.9.0" version = "0.9.0"
@@ -5999,6 +6148,12 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@@ -6315,6 +6470,18 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web_atoms"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414"
dependencies = [
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
]
[[package]] [[package]]
name = "webp" name = "webp"
version = "0.3.1" version = "0.3.1"

View File

@@ -16,3 +16,4 @@ tracing = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
url = { version = "2", features = ["serde"] } url = { version = "2", features = ["serde"] }
ammonia = "4"

View File

@@ -45,7 +45,7 @@ impl ApObjectHandler for CompositeObjectHandler {
} else if is_watchlist { } else if is_watchlist {
self.watchlist.on_create(ap_id, actor_url, object).await self.watchlist.on_create(ap_id, actor_url, object).await
} else { } else {
tracing::debug!(ap_id = %ap_id, "ignoring Create for unknown object type"); tracing::warn!(ap_id = %ap_id, "ignoring Create for unknown object type");
Ok(()) Ok(())
} }
} }

View File

@@ -45,8 +45,8 @@ impl k_ap::EventPublisher for FederationEventBridge {
.await .await
.map_err(|e| anyhow::anyhow!(e.to_string())) .map_err(|e| anyhow::anyhow!(e.to_string()))
} }
other => { FederationEvent::DeliveryFailed { inbox, error, .. } => {
tracing::debug!("ignoring federation event: {:?}", other); tracing::warn!(inbox = %inbox, error = %error, "federation delivery failed permanently");
Ok(()) Ok(())
} }
} }

View File

@@ -77,13 +77,15 @@ impl ApObjectHandler for ReviewObjectHandler {
_actor_url: &Url, _actor_url: &Url,
object: serde_json::Value, object: serde_json::Value,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let obj: ReviewObject = match serde_json::from_value(object) { let mut obj: ReviewObject = match serde_json::from_value(object) {
Ok(o) => o, Ok(o) => o,
Err(e) => { Err(e) => {
tracing::debug!("ignoring unrecognized Create object: {}", e); tracing::warn!("ignoring unrecognized Create object: {}", e);
return Ok(()); return Ok(());
} }
}; };
obj.movie_title = ammonia::clean(&obj.movie_title);
obj.comment = obj.comment.map(|c| ammonia::clean(&c));
let actor_url_str = obj.attributed_to.to_string(); let actor_url_str = obj.attributed_to.to_string();
let review_id = ReviewId::generate(); let review_id = ReviewId::generate();
@@ -130,13 +132,15 @@ impl ApObjectHandler for ReviewObjectHandler {
actor_url: &Url, actor_url: &Url,
object: serde_json::Value, object: serde_json::Value,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let obj: ReviewObject = match serde_json::from_value(object) { let mut obj: ReviewObject = match serde_json::from_value(object) {
Ok(o) => o, Ok(o) => o,
Err(_) => { Err(_) => {
tracing::debug!(actor = %actor_url, "ignoring non-review Update activity"); tracing::warn!(actor = %actor_url, "ignoring non-review Update activity");
return Ok(()); return Ok(());
} }
}; };
obj.movie_title = ammonia::clean(&obj.movie_title);
obj.comment = obj.comment.map(|c| ammonia::clean(&c));
if obj.attributed_to != *actor_url { if obj.attributed_to != *actor_url {
anyhow::bail!("update actor does not match object attributed_to"); anyhow::bail!("update actor does not match object attributed_to");

View File

@@ -24,7 +24,14 @@ impl ApObjectHandler for WatchlistObjectHandler {
actor_url: &Url, actor_url: &Url,
object: serde_json::Value, object: serde_json::Value,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let obj: WatchlistObject = serde_json::from_value(object)?; let mut obj: WatchlistObject = match serde_json::from_value(object) {
Ok(o) => o,
Err(e) => {
tracing::warn!(ap_id = %ap_id, "ignoring malformed watchlist Create: {}", e);
return Ok(());
}
};
obj.movie_title = ammonia::clean(&obj.movie_title);
let added_at = obj.published; let added_at = obj.published;
let entry = RemoteWatchlistEntry { let entry = RemoteWatchlistEntry {
ap_id: ap_id.as_str().to_string(), ap_id: ap_id.as_str().to_string(),

View File

@@ -12,7 +12,7 @@ pub fn build_router(state: AppState, ap_router: Router) -> Router {
let ap_cfg = GovernorConfigBuilder::default() let ap_cfg = GovernorConfigBuilder::default()
.with_extractor(PeerIp::default()) .with_extractor(PeerIp::default())
.expect_connect_info() .expect_connect_info()
.quota_default(per_minute(rate_limit)) .quota_default(per_minute(rate_limit / 2))
.finish() .finish()
.unwrap(); .unwrap();
let ap_router = ap_router.layer(GovernorLayer::new(ap_cfg)); let ap_router = ap_router.layer(GovernorLayer::new(ap_cfg));