An unauthenticated GET /ipfs/{cid} can be served raw sqlx/Postgres error text (schema, constraint, or connection detail) in the 500 response body when one of the handler's initial metadata queries errors. Same class as #106, different surface (that one is git-endpoint filesystem paths; this is the /ipfs surface leaking DB internals).
Mechanism
get_by_cid (crates/gitlawb-node/src/api/ipfs.rs) maps both metadata-query errors to AppError::Internal(e) carrying the raw error:
ipfs.rs:170 Ok(Err(e)) => return Err(AppError::Internal(e)) (list_all_repos)
ipfs.rs:194 Ok(Err(e)) => return Err(AppError::Internal(e)) (list_visibility_rules_for_repos)
The DB methods propagate the sqlx error with a bare .await? (no .context()), so the wrapped anyhow::Error's to_string() is the sqlx Error Display verbatim.
error.rs:160 renders it into the client body: AppError::Internal(e) => (INTERNAL_SERVER_ERROR, "internal_error", e.to_string()), serialized as {"error","message"}. Because these arms construct Internal directly (not via ?/.into()), they also bypass the From<anyhow::Error> downcast and the opaque db_unavailable -> 503 handling, so even a connection-level failure renders raw.
The route is unauthenticated: auth: Option<...>, and the two queries run before any per-repo visibility check, after only the walk-permit admission.
Reachability / trigger
Any query- or connection-level error on those two queries (schema drift, a permissions issue, a dropped/exhausted pool connection, a statement error) returns Postgres schema/connection text to an anonymous caller.
Notes
Fix
Opaque-ify AppError::Internal response bodies (log the detail, return a generic message) — this also resolves #106 — or at minimum stop passing the raw sqlx error through these two arms.
An unauthenticated
GET /ipfs/{cid}can be served raw sqlx/Postgres error text (schema, constraint, or connection detail) in the 500 response body when one of the handler's initial metadata queries errors. Same class as #106, different surface (that one is git-endpoint filesystem paths; this is the /ipfs surface leaking DB internals).Mechanism
get_by_cid(crates/gitlawb-node/src/api/ipfs.rs) maps both metadata-query errors toAppError::Internal(e)carrying the raw error:ipfs.rs:170Ok(Err(e)) => return Err(AppError::Internal(e))(list_all_repos)ipfs.rs:194Ok(Err(e)) => return Err(AppError::Internal(e))(list_visibility_rules_for_repos)The DB methods propagate the sqlx error with a bare
.await?(no.context()), so the wrappedanyhow::Error'sto_string()is the sqlxErrorDisplay verbatim.error.rs:160renders it into the client body:AppError::Internal(e) => (INTERNAL_SERVER_ERROR, "internal_error", e.to_string()), serialized as{"error","message"}. Because these arms constructInternaldirectly (not via?/.into()), they also bypass theFrom<anyhow::Error>downcast and the opaquedb_unavailable-> 503 handling, so even a connection-level failure renders raw.The route is unauthenticated:
auth: Option<...>, and the two queries run before any per-repo visibility check, after only the walk-permit admission.Reachability / trigger
Any query- or connection-level error on those two queries (schema drift, a permissions issue, a dropped/exhausted pool connection, a statement error) returns Postgres schema/connection text to an anonymous caller.
Notes
.map_err(AppError::Internal)?); the feat(node,git): cap concurrent served git ops with a 503 load-shed (#62) #174 F6 change only wrapped these calls in a request-budget timeout, keeping the error mapping byte-for-byte. Filing here because it was surfaced during that review and is not otherwise tracked for this surface.error.rs:160rendering raw internal text). A fix that opaque-ifiesAppError::Internalbodies (generic message to the client, detail to the log) closes both. Worth noting the internal inconsistency: this same handler already routes raw git stderr to an opaqueAppError::Git(ipfs.rs:397, 576) to avoid leaking filesystem paths, but leaves these DB-error arms raw.Fix
Opaque-ify
AppError::Internalresponse bodies (log the detail, return a generic message) — this also resolves #106 — or at minimum stop passing the raw sqlx error through these two arms.