Strip CR/LF/NUL from every outbound s2s line

Free-form trailing text (message bodies, kick reasons, topics, metadata)
now passes through a single sanitizing choke-point in the serializer, so
no such text can carry an embedded line break that would smuggle a second
command onto the link. Line-based input closes the obvious path today,
but sanitizing at the point of serialization removes the whole class
regardless of where the text originated (federation, config, future
non-line sources).
This commit is contained in:
Jean Chevronnet 2026-07-13 23:34:51 +00:00
parent 2fd5676ff2
commit 5d67133295
No known key found for this signature in database

View file

@ -227,7 +227,7 @@ impl Protocol for InspIrcd {
} }
fn serialize(&mut self, action: &NetAction) -> Vec<String> { fn serialize(&mut self, action: &NetAction) -> Vec<String> {
match action { let lines = match action {
NetAction::Burst => vec![self.sourced(format!("BURST {}", self.ts))], NetAction::Burst => vec![self.sourced(format!("BURST {}", self.ts))],
NetAction::EndBurst => vec![self.sourced("ENDBURST".to_string())], NetAction::EndBurst => vec![self.sourced("ENDBURST".to_string())],
NetAction::Pong { token, from } => { NetAction::Pong { token, from } => {
@ -321,7 +321,12 @@ impl Protocol for InspIrcd {
NetAction::Raw(s) => vec![s.clone()], NetAction::Raw(s) => vec![s.clone()],
// Internal: the link layer handles these before serialization. // Internal: the link layer handles these before serialization.
NetAction::DeferRegister { .. } | NetAction::DeferPassword { .. } | NetAction::SendEmail { .. } => vec![], NetAction::DeferRegister { .. } | NetAction::DeferPassword { .. } | NetAction::SendEmail { .. } => vec![],
} };
// A trailing parameter can carry free-form text (message bodies, kick
// reasons, topics, metadata). Strip CR/LF/NUL at this single choke-point
// so no such text — whatever its origin — can smuggle a second command
// onto the s2s link.
lines.into_iter().map(|l| l.replace(['\r', '\n', '\0'], "")).collect()
} }
fn sid(&self) -> &str { fn sid(&self) -> &str {
@ -467,6 +472,19 @@ mod tests {
assert_eq!(lines, vec![":42S ENCAP 0IR CHGIDENT 0IRAAAAAB cool".to_string()]); assert_eq!(lines, vec![":42S ENCAP 0IR CHGIDENT 0IRAAAAAB cool".to_string()]);
} }
// Free-form text with embedded line breaks must not smuggle a second line
// onto the link: the serializer strips CR/LF/NUL from every outbound line.
#[test]
fn strips_crlf_from_trailing_text() {
let lines = proto().serialize(&NetAction::Notice {
from: "42SAAAAAA".into(),
to: "0IRAAAAAB".into(),
text: "hi\r\nKILL 0IRAAAAAB :pwned".into(),
});
assert_eq!(lines, vec![":42SAAAAAA NOTICE 0IRAAAAAB :hiKILL 0IRAAAAAB :pwned".to_string()]);
assert!(!lines[0].contains('\n') && !lines[0].contains('\r'));
}
#[test] #[test]
fn parses_ftopic_and_filters_own() { fn parses_ftopic_and_filters_own() {
let ev = proto().parse(":0IRAAAAAB FTOPIC #chan 1783845132 1783845140 :Welcome all"); let ev = proto().parse(":0IRAAAAAB FTOPIC #chan 1783845132 1783845140 :Welcome all");