From 063a5e689fd07dbaf4bede80ae2b0095e2636bd7 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Wed, 26 Aug 2026 16:22:29 +0600 Subject: [PATCH] fix(node): gate /ipfs/pins and /arweave/anchors behind authentication (#134) Add auth rejection to list_pins and list_anchors handlers. The pin index spans the entire node and would expose metadata for every object ever pushed; anonymous callers must not see it. - Require AuthenticatedDid extension in both handlers, return 401 when absent - Add server.rs regression test for anonymous rejection through build_router - Fix closed-pool tests to pass authenticated requests (test 503 path) --- crates/gitlawb-node/src/api/arweave.rs | 9 +++++- crates/gitlawb-node/src/api/ipfs.rs | 11 ++++++- crates/gitlawb-node/src/server.rs | 42 ++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/crates/gitlawb-node/src/api/arweave.rs b/crates/gitlawb-node/src/api/arweave.rs index ad8f45a73..dc3690507 100644 --- a/crates/gitlawb-node/src/api/arweave.rs +++ b/crates/gitlawb-node/src/api/arweave.rs @@ -1,7 +1,7 @@ //! GET /api/v1/arweave/anchors — list Arweave ref-update anchors. use axum::{ - extract::{Query, State}, + extract::{Extension, Query, State}, Json, }; use serde::Deserialize; @@ -24,7 +24,13 @@ fn default_limit() -> i64 { pub async fn list_anchors( State(state): State, Query(q): Query, + auth: Option>, ) -> Result> { + if auth.is_none() { + return Err(crate::error::AppError::Unauthorized( + "authentication required for anchor listing".into(), + )); + } let limit = q.limit.min(200); // Bare `?` so connection-class sqlx failures downcast to `AppError::Db` and // map to 503 `db_unavailable` (not 500 via `.map_err(AppError::Internal)`) (#251). @@ -60,6 +66,7 @@ mod closed_pool_tests { .oneshot( Request::builder() .uri("/api/v1/arweave/anchors") + .extension(crate::auth::AuthenticatedDid("did:key:test".into())) .body(axum::body::Body::empty()) .unwrap(), ) diff --git a/crates/gitlawb-node/src/api/ipfs.rs b/crates/gitlawb-node/src/api/ipfs.rs index 92d129803..c2e1db27a 100644 --- a/crates/gitlawb-node/src/api/ipfs.rs +++ b/crates/gitlawb-node/src/api/ipfs.rs @@ -2133,7 +2133,15 @@ async fn gate_and_serve( /// Returns all CIDs that have been pinned to the local IPFS node from git /// objects received via push. Each entry includes the git SHA-256 hex, the /// CIDv1 string, and the timestamp when it was pinned. -pub async fn list_pins(State(state): State) -> Result> { +pub async fn list_pins( + State(state): State, + auth: Option>, +) -> Result> { + if auth.is_none() { + return Err(crate::error::AppError::Unauthorized( + "authentication required for pin listing".into(), + )); + } // Bare `?` so connection-class sqlx failures downcast to `AppError::Db` and // map to 503 `db_unavailable` (not 500 via `.map_err(AppError::Internal)`) (#251). let pins = state.db.list_pinned_cids().await?; @@ -2426,6 +2434,7 @@ mod closed_pool_tests { .oneshot( Request::builder() .uri("/api/v1/ipfs/pins") + .extension(crate::auth::AuthenticatedDid("did:key:test".into())) .body(axum::body::Body::empty()) .unwrap(), ) diff --git a/crates/gitlawb-node/src/server.rs b/crates/gitlawb-node/src/server.rs index de61fcbe2..330015bae 100644 --- a/crates/gitlawb-node/src/server.rs +++ b/crates/gitlawb-node/src/server.rs @@ -619,3 +619,45 @@ async fn p2p_info(State(state): State) -> Json { None => Json(json!({ "enabled": false })), } } + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + use axum::http::{Request, StatusCode}; + use sqlx::PgPool; + use tower::ServiceExt; + + use crate::test_support::test_state; + + /// Regression: anonymous callers must not see the pin/anchor index (#121, #134). + #[sqlx::test] + async fn unsigned_get_pins_and_anchors_is_401_through_build_router(pool: PgPool) { + let state = test_state(pool).await; + let router = build_router(state); + + let pins = Request::builder() + .method("GET") + .uri("/api/v1/ipfs/pins?limit=50") + .body(Body::empty()) + .unwrap(); + let pins_resp = router.clone().oneshot(pins).await.unwrap(); + assert_eq!( + pins_resp.status(), + StatusCode::UNAUTHORIZED, + "anonymous pin listing must be rejected" + ); + + let anchors = Request::builder() + .method("GET") + .uri("/api/v1/arweave/anchors?limit=50") + .body(Body::empty()) + .unwrap(); + let anchors_resp = router.oneshot(anchors).await.unwrap(); + assert_eq!( + anchors_resp.status(), + StatusCode::UNAUTHORIZED, + "anonymous anchors listing must be rejected" + ); + } +}