Skip to main content

lightning_types/
string.rs

1// This file is Copyright its original authors, visible in version control
2// history.
3//
4// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7// You may not use this file except in accordance with one or both of these
8// licenses.
9
10//! Utilities for strings.
11
12use alloc::string::String;
13use core::fmt;
14
15use crate::unicode::*;
16
17/// Struct to `Display` fields in a safe way using `PrintableString`
18#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
19pub struct UntrustedString(pub String);
20
21impl fmt::Display for UntrustedString {
22	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23		PrintableString(&self.0).fmt(f)
24	}
25}
26
27/// A string that displays only printable characters, replacing control characters with
28/// [`core::char::REPLACEMENT_CHARACTER`].
29#[derive(Debug, PartialEq)]
30pub struct PrintableString<'a>(pub &'a str);
31
32impl<'a> fmt::Display for PrintableString<'a> {
33	fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
34		use core::fmt::Write;
35		for c in self.0.chars() {
36			let is_other = is_unicode_general_category_other(c);
37			let is_unassigned = is_unicode_general_category_unassigned(c);
38			// U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR (general
39			// categories `Zl`/`Zp`) are covered by neither `char::is_control`
40			// (`Cc` only) nor the top-level `C` tables above, but terminals and
41			// log viewers commonly render them as hard line breaks, allowing an
42			// attacker-controlled string to inject forged lines into operator
43			// logs — so the generated separator table filters them as well.
44			let is_line_separator = is_unicode_general_category_separator(c);
45			let c = if c.is_control() || is_other || is_unassigned || is_line_separator {
46				core::char::REPLACEMENT_CHARACTER
47			} else {
48				c
49			};
50			f.write_char(c)?;
51		}
52
53		Ok(())
54	}
55}
56
57#[cfg(test)]
58mod tests {
59	use super::PrintableString;
60
61	#[test]
62	fn displays_printable_string() {
63		assert_eq!(
64			format!("{}", PrintableString("I \u{1F496} LDK!\t\u{26A1}")),
65			"I \u{1F496} LDK!\u{FFFD}\u{26A1}",
66		);
67	}
68
69	#[test]
70	fn sanitizes_unicode_bidi_override_characters() {
71		// U+202E RIGHT-TO-LEFT OVERRIDE and friends are Unicode general category
72		// `Cf` (Format), not `Cc` (Control). They enable "Trojan Source" /
73		// bidi-spoofing attacks where an attacker-supplied string (e.g. a node
74		// alias gossiped from a peer) renders to a human reader as something
75		// other than its byte content. `PrintableString` is the sanitiser used
76		// for exactly these untrusted strings, so it must replace them.
77		let rendered = format!("{}", PrintableString("safe\u{202E}cipsxe.exe"));
78		assert!(
79			!rendered.contains('\u{202E}'),
80			"PrintableString left a U+202E RLO override in its output: {:?}",
81			rendered
82		);
83
84		// U+13440 is in the Egyptian Hieroglyph Format Controls block, but its
85		// general category is `Mn`, not `Cf`, so the `Cf` range ends at U+1343F.
86		assert_eq!(format!("{}", PrintableString("x\u{1343F}y\u{13440}z")), "x\u{FFFD}y\u{13440}z");
87	}
88
89	#[test]
90	fn sanitizes_line_and_paragraph_separators() {
91		// U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are general
92		// categories `Zl`/`Zp`, not `Cc`, so `char::is_control` does not catch
93		// them, yet terminals and log viewers commonly render them as hard line
94		// breaks. As with the bidi overrides above, an attacker-controlled string
95		// must not be able to use them to inject forged log lines.
96		assert_eq!(
97			format!("{}", PrintableString("ok\u{2028}forged\u{2029}more")),
98			"ok\u{FFFD}forged\u{FFFD}more"
99		);
100	}
101}