security(text): keep font scans within configured folders (#2876)

* security(text): keep font scans within configured folders

* fonts: still find symlinked font files; skip only symlinked folders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fonts: rustfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Earl Co
2026-10-23 10:52:49 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent d3ec344691
commit 68cfb0b52d
3 changed files with 36 additions and 2 deletions
Generated
+1
View File
@@ -2287,6 +2287,7 @@ dependencies = [
"filmcraft-color",
"harfrust",
"skrifa",
"tempfile",
"unicode-bidi",
"unicode-linebreak",
"unicode-segmentation",
+3
View File
@@ -21,3 +21,6 @@ unicode-bidi = "0.3.18"
unicode-linebreak = "0.1.5"
# UAX #29 extended grapheme clusters (MIT OR Apache-2.0).
unicode-segmentation = "1.13"
[dev-dependencies]
tempfile = "3.27"
+32 -2
View File
@@ -114,16 +114,46 @@ fn walk(dir: &std::path::Path, depth: u32, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
let Ok(file_type) = e.file_type() else { continue };
// Font discovery is bounded to the configured folders: never follow a symlinked folder
// into a loop or an unrelated tree. Symlinked font files (NixOS, home-manager, Debian
// alternatives) still count.
let is_file = if file_type.is_symlink() { std::fs::metadata(&p).is_ok_and(|m| m.is_file()) } else { file_type.is_file() };
if file_type.is_dir() {
if depth < 4 {
walk(&p, depth + 1, out);
}
} else if p.extension().and_then(|x| x.to_str()).is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")) {
} else if is_file && p.extension().and_then(|x| x.to_str()).is_some_and(|x| matches!(x.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc")) {
out.push(p);
}
}
}
#[cfg(all(test, unix))]
mod directory_tests {
use super::walk;
#[test]
fn directory_source_does_not_follow_symlinked_font_trees() {
use std::os::unix::fs::symlink;
let root = tempfile::tempdir().unwrap();
let configured = root.path().join("configured");
let foreign = root.path().join("foreign");
std::fs::create_dir_all(&configured).unwrap();
std::fs::create_dir_all(&foreign).unwrap();
std::fs::write(configured.join("inside.ttf"), b"").unwrap();
std::fs::write(foreign.join("outside.ttf"), b"").unwrap();
symlink(&foreign, configured.join("loop")).unwrap();
symlink(foreign.join("outside.ttf"), configured.join("linked.ttf")).unwrap();
let mut found = Vec::new();
walk(&configured, 0, &mut found);
found.sort();
assert_eq!(found, vec![configured.join("inside.ttf"), configured.join("linked.ttf")]);
}
}
impl FontSource for DirectorySource {
fn faces(&self) -> Vec<FaceInfo> {
let mut files = Vec::new();