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:
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")));
+ }
}