lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 4ef4e07e485124c262e4b4a2e7093f801bb53d53
parent 24c20687d6c6ef1a108be32f5cb515f45eaf9684
Author: triesap <tyson@radroots.org>
Date:   Mon,  5 Jan 2026 20:15:16 +0000

list-set: validate base64 ids in d tags



- Enforce base64url validation for farm/coop/resource d tags
- Reject invalid d_tag values during decode
- Reject invalid d_tag values during encode
- Add unit tests covering invalid d_tag cases

Diffstat:
Mevents-codec/src/list_set/decode.rs | 3+++
Mevents-codec/src/list_set/encode.rs | 3+++
Mevents-codec/src/list_set/mod.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 52 insertions(+), 0 deletions(-)

diff --git a/events-codec/src/list_set/decode.rs b/events-codec/src/list_set/decode.rs @@ -92,6 +92,9 @@ pub fn list_set_from_tags( } let d_tag = d_tag.ok_or(EventParseError::MissingTag("d"))?; + if !super::list_set_base64_id_is_valid(&d_tag) { + return Err(EventParseError::InvalidTag("d")); + } Ok(RadrootsListSet { d_tag, content, diff --git a/events-codec/src/list_set/encode.rs b/events-codec/src/list_set/encode.rs @@ -27,6 +27,9 @@ pub fn list_set_build_tags(list: &RadrootsListSet) -> Result<Vec<Vec<String>>, E if list.d_tag.trim().is_empty() { return Err(EventEncodeError::EmptyRequiredField("d_tag")); } + if !super::list_set_base64_id_is_valid(&list.d_tag) { + return Err(EventEncodeError::InvalidField("d_tag")); + } let mut tags = Vec::with_capacity(1 + list.entries.len() + 3); push_tag(&mut tags, TAG_D, &list.d_tag); if let Some(title) = list.title.as_ref().filter(|v| !v.trim().is_empty()) { diff --git a/events-codec/src/list_set/mod.rs b/events-codec/src/list_set/mod.rs @@ -1,9 +1,27 @@ pub mod decode; pub mod encode; +use crate::d_tag::is_d_tag_base64url; + +fn list_set_requires_base64(d_tag: &str) -> bool { + d_tag.starts_with("farm:") || d_tag.starts_with("coop:") || d_tag.starts_with("resource:") +} + +fn list_set_base64_id_is_valid(d_tag: &str) -> bool { + if !list_set_requires_base64(d_tag) { + return true; + } + let mut parts = d_tag.splitn(3, ':'); + let _ = parts.next(); + let id = parts.next().unwrap_or(""); + let suffix = parts.next().unwrap_or(""); + !id.trim().is_empty() && !suffix.trim().is_empty() && is_d_tag_base64url(id) +} + #[cfg(test)] mod tests { use super::{decode::list_set_from_tags, encode::list_set_build_tags}; + use crate::error::{EventEncodeError, EventParseError}; use radroots_events::{ kinds::KIND_LIST_SET_FOLLOW, list::{RadrootsListEntry}, @@ -37,4 +55,32 @@ mod tests { assert_eq!(parsed.entries.len(), list.entries.len()); assert_eq!(parsed.entries[0].values[0], "owner_pubkey"); } + + #[test] + fn list_set_rejects_invalid_farm_d_tag_on_encode() { + let list = RadrootsListSet { + d_tag: "farm:invalid:members".to_string(), + content: "".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec!["pubkey".to_string()], + }], + title: None, + description: None, + image: None, + }; + let err = list_set_build_tags(&list).expect_err("expected invalid d_tag"); + assert!(matches!(err, EventEncodeError::InvalidField("d_tag"))); + } + + #[test] + fn list_set_rejects_invalid_farm_d_tag_on_decode() { + let tags = vec![ + vec!["d".to_string(), "farm:invalid:members".to_string()], + vec!["p".to_string(), "pubkey".to_string()], + ]; + let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "".to_string(), &tags) + .expect_err("expected invalid d_tag"); + assert!(matches!(err, EventParseError::InvalidTag("d"))); + } }