Skip to content

Commit 9f366c2

Browse files
committed
Screen v1 requests in PUT requests
Since the receiver's proposal contains new outputs and inputs contributed by the receiver, that PSBT should also be screened.
1 parent f92ae86 commit 9f366c2

1 file changed

Lines changed: 60 additions & 30 deletions

File tree

payjoin-directory/src/lib.rs

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,30 @@ impl<D: Db> Service<D> {
428428
let timeout_response = Response::builder().status(StatusCode::ACCEPTED).body(empty())?;
429429
handle_peek(self.db.wait_for_v2_payload(&id).await, timeout_response)
430430
}
431+
432+
/// Screen a V1 PSBT body against the address blocklist.
433+
///
434+
/// Returns `Ok(())` if screening passes or is not configured.
435+
async fn check_v1_blocklist(&self, body_str: &str) -> Result<(), HandlerError> {
436+
if let Some(blocked) = self.v1.as_ref().and_then(|v| v.blocked_addresses.as_ref()) {
437+
let scripts = blocked.0.read().await;
438+
if !scripts.is_empty() {
439+
match screen_v1_addresses(body_str, &scripts) {
440+
ScreenResult::Blocked => {
441+
return Err(HandlerError::Forbidden(anyhow::anyhow!(
442+
"blocked address in V1 PSBT"
443+
)));
444+
}
445+
ScreenResult::Clean => {}
446+
ScreenResult::ParseError(e) => {
447+
warn!("Could not parse V1 PSBT: {e}");
448+
}
449+
}
450+
}
451+
}
452+
Ok(())
453+
}
454+
431455
async fn put_payjoin_v1(
432456
&self,
433457
id: &str,
@@ -446,6 +470,9 @@ impl<D: Db> Service<D> {
446470
return Err(HandlerError::PayloadTooLarge);
447471
}
448472

473+
let body_str = std::str::from_utf8(&req).map_err(|e| HandlerError::BadRequest(e.into()))?;
474+
self.check_v1_blocklist(body_str).await?;
475+
449476
match self.db.post_v1_response(&id, req.into()).await {
450477
Ok(_) => Ok(ok_response),
451478
Err(e) => Err(HandlerError::BadRequest(e.into())),
@@ -479,23 +506,7 @@ impl<D: Db> Service<D> {
479506
Err(_) => return Ok(bad_request_body_res),
480507
};
481508

482-
if let Some(blocked) = self.v1.as_ref().and_then(|v| v.blocked_addresses.as_ref()) {
483-
let scripts = blocked.0.read().await;
484-
if !scripts.is_empty() {
485-
match screen_v1_addresses(&body_str, &scripts) {
486-
ScreenResult::Blocked => {
487-
return Ok(Response::builder()
488-
.status(StatusCode::FORBIDDEN)
489-
.body(empty())?);
490-
}
491-
ScreenResult::Clean => {}
492-
ScreenResult::ParseError(e) => {
493-
warn!("Could not screen V1 payload: {e}");
494-
// fail-open: unparsable PSBTs can't complete transactions
495-
}
496-
}
497-
}
498-
}
509+
self.check_v1_blocklist(&body_str).await?;
499510

500511
let v2_compat_body = format!("{body_str}\n{query}");
501512
let id = ShortId::from_str(id)?;
@@ -790,6 +801,8 @@ mod tests {
790801
(parts.status, String::from_utf8(bytes.to_vec()).unwrap())
791802
}
792803

804+
// V1 routing
805+
793806
#[tokio::test]
794807
async fn post_v1_when_disabled_returns_version_unsupported() {
795808
let mut svc = test_service(None).await;
@@ -840,24 +853,17 @@ mod tests {
840853
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
841854
assert_eq!(body, V1_UNAVAILABLE_RES_JSON);
842855
}
843-
}
844856

845-
#[cfg(test)]
846-
mod screen_tests {
847-
use super::*;
848-
849-
fn addr_to_script(address: &str) -> bitcoin::ScriptBuf {
850-
let addr: bitcoin::Address<bitcoin::address::NetworkUnchecked> =
851-
address.parse().expect("valid address");
852-
addr.assume_checked().script_pubkey()
853-
}
857+
// Address screening
854858

855859
fn make_test_psbt_base64(output_address: &str) -> String {
856860
use bitcoin::base64::prelude::{Engine, BASE64_STANDARD};
857861
use bitcoin::psbt::Psbt;
858862
use bitcoin::{Amount, Transaction, TxIn, TxOut};
859863

860-
let script_pubkey = addr_to_script(output_address);
864+
let addr: bitcoin::Address<bitcoin::address::NetworkUnchecked> =
865+
output_address.parse().expect("valid address");
866+
let script_pubkey = addr.assume_checked().script_pubkey();
861867

862868
let tx = Transaction {
863869
version: bitcoin::transaction::Version::TWO,
@@ -867,8 +873,32 @@ mod screen_tests {
867873
};
868874

869875
let psbt = Psbt::from_unsigned_tx(tx).expect("valid psbt");
870-
let serialized = psbt.serialize();
871-
BASE64_STANDARD.encode(&serialized)
876+
BASE64_STANDARD.encode(psbt.serialize())
877+
}
878+
879+
fn addr_to_script(address: &str) -> bitcoin::ScriptBuf {
880+
let addr: bitcoin::Address<bitcoin::address::NetworkUnchecked> =
881+
address.parse().expect("valid address");
882+
addr.assume_checked().script_pubkey()
883+
}
884+
885+
#[tokio::test]
886+
async fn post_v1_with_blocked_address_returns_forbidden() {
887+
let blocked_addr = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
888+
let blocked = BlockedAddresses::from_address_lines(blocked_addr);
889+
let mut svc = test_service(Some(V1::new(Some(blocked)))).await;
890+
let id = valid_short_id_path();
891+
let psbt_b64 = make_test_psbt_base64(blocked_addr);
892+
let req = Request::builder()
893+
.method(Method::POST)
894+
.uri(format!("http://localhost/{id}"))
895+
.body(Full::new(Bytes::from(psbt_b64)))
896+
.unwrap();
897+
898+
let res = tower::Service::call(&mut svc, req).await.unwrap();
899+
let (status, _body) = collect_body(res).await;
900+
901+
assert_eq!(status, StatusCode::FORBIDDEN);
872902
}
873903

874904
#[test]

0 commit comments

Comments
 (0)