node: Ensure private RIDs don't leak in gossip

Though refs of private repositories are not announced publicly, they can
sometimes be relayed to nodes that are not in the allow list.

We fix this by always checking the visibility of a repository before
sending a refs announcement of it to a peer.

Discovered-by: Adrian Duke <adrian.duke@gmail.com>
This commit is contained in:
cloudhead 2024-10-07 18:11:59 +02:00
parent 3acdb17b86
commit 034eb41860
No known key found for this signature in database
2 changed files with 102 additions and 9 deletions

View File

@ -2312,9 +2312,14 @@ where
self.last_timestamp
}
fn relay(&mut self, id: gossip::AnnouncementId, msg: Announcement) {
let announcer = msg.node;
fn relay(&mut self, id: gossip::AnnouncementId, ann: Announcement) {
let announcer = ann.node;
let relayed_by = self.relayed_by.get(&id);
let rid = if let AnnouncementMessage::Refs(RefsAnnouncement { rid, .. }) = ann.message {
Some(rid)
} else {
None
};
// Choose peers we should relay this message to.
// 1. Don't relay to a peer who sent us this message.
// 2. Don't relay to the peer who signed this announcement.
@ -2327,9 +2332,25 @@ where
.unwrap_or(true) // If there are no relayers we let it through.
})
.filter(|(id, _)| **id != announcer)
.filter(|(id, _)| {
if let Some(rid) = rid {
// Only relay this message if the peer is allowed to know about the
// repository. If we don't have the repository, return `false` because
// we can't determine if it's private or public.
self.storage
.get(rid)
.ok()
.flatten()
.map(|doc| doc.is_visible_to(id))
.unwrap_or(false)
} else {
// Announcement doesn't concern a specific repository, let it through.
true
}
})
.map(|(_, p)| p);
self.outbox.relay(msg, relay_to);
self.outbox.relay(ann, relay_to);
}
////////////////////////////////////////////////////////////////////////////

View File

@ -671,13 +671,9 @@ fn test_announcement_relay() {
}
#[test]
fn test_refs_announcement_relay() {
fn test_refs_announcement_relay_public() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Peer::with_storage(
"alice",
[7, 7, 7, 7],
Storage::open(tmp.path().join("alice"), fixtures::user()).unwrap(),
);
let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], MockStorage::empty());
let eve = Peer::with_storage(
"eve",
[8, 8, 8, 8],
@ -713,6 +709,12 @@ fn test_refs_announcement_relay() {
.receive(bob.id(), bob.refs_announcement(bob_inv[0]))
.elapse(service::GOSSIP_INTERVAL);
// Pretend Alice cloned Bob's repos.
let repos = gen::<[MockRepository; 3]>(1);
for (i, mut repo) in repos.into_iter().enumerate() {
repo.doc.doc.visibility = Visibility::Public; // Public repos are always gossiped.
alice.storage_mut().repos.insert(bob_inv[i], repo);
}
assert_matches!(
alice.messages(eve.id()).next(),
Some(Message::Announcement(_)),
@ -746,6 +748,76 @@ fn test_refs_announcement_relay() {
);
}
#[test]
fn test_refs_announcement_relay_private() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], MockStorage::empty());
let eve = Peer::with_storage(
"eve",
[8, 8, 8, 8],
Storage::open(tmp.path().join("eve"), fixtures::user()).unwrap(),
);
let bob = {
let mut rng = fastrand::Rng::new();
let signer = MockSigner::new(&mut rng);
let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
Peer::config(
"bob",
[9, 9, 9, 9],
storage,
peer::Config {
signer,
rng,
..peer::Config::default()
},
)
.initialized()
};
let bob_inv = bob.inventory().into_iter().collect::<Vec<_>>();
alice.seed(&bob_inv[0], policy::Scope::All).unwrap();
alice.seed(&bob_inv[1], policy::Scope::All).unwrap();
alice.connect_to(&bob);
alice.connect_to(&eve);
alice.receive(eve.id(), Message::Subscribe(Subscribe::all()));
// The first repo is not visible to Eve.
let mut repo1 = gen::<MockRepository>(1);
repo1.doc.doc.visibility = Visibility::Private { allow: [].into() };
alice.storage_mut().repos.insert(bob_inv[0], repo1);
// The second repo is visible to Eve.
let mut repo2 = gen::<MockRepository>(1);
repo2.doc.doc.visibility = Visibility::Private {
allow: [eve.id.into()].into(),
};
alice.storage_mut().repos.insert(bob_inv[1], repo2);
alice.elapse(service::GOSSIP_INTERVAL);
alice.messages(eve.id()).for_each(drop);
alice
.receive(bob.id(), bob.refs_announcement(bob_inv[0]))
.elapse(service::GOSSIP_INTERVAL);
assert_matches!(
alice.messages(eve.id()).next(),
None,
"The first ref announcement is not relayed to Eve"
);
alice
.receive(bob.id(), bob.refs_announcement(bob_inv[1]))
.elapse(service::GOSSIP_INTERVAL);
assert_matches!(
alice.messages(eve.id()).next(),
Some(Message::Announcement(Announcement {
message: AnnouncementMessage::Refs(_),
..
})),
"The second ref announcement is relayed to Eve"
);
}
/// Even if Alice is not tracking Bob, Alice will fetch Bob's refs for a repo she doesn't have.
#[test]
fn test_refs_announcement_fetch_trusted_no_inventory() {