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}