Skip to main content

slipcase_open/
content.rs

1//! The one content check, and everything it deliberately does not do.
2//
3// Author: David M. Anderson
4// Built with AI assistance (Claude, Anthropic)
5//
6//! Concept 5.1 settles that policy keys on the extension, because that is what
7//! `ShellExecuteEx`, `open` and `xdg-open` resolve a handler from and none of
8//! them reads the bytes. Sniffing a content type and checking policy against it
9//! would be checking a value with no bearing on what executes.
10//!
11//! What survives is narrow and is not policy. It reports a content file whose
12//! bytes are an executable image or a script under a name that claims neither
13//! — the shape of a phishing attachment.
14//!
15//! **[`crate::flow`] refuses on it, and that is a veto rather than a control.**
16//! Nothing here permits anything: the allowlist decides what may be opened, and
17//! all this can do is say no to something it already allowed. So it is allowed
18//! to be narrow in a way a control could not be — a content file that is
19//! exactly what it claims and still hostile passes without comment, and that
20//! is not a gap in it, because it was never the thing standing in the way.
21//!
22//! **It is a handful of magic numbers and not a type table.** A `.docx`
23//! sniffing as a ZIP is noise and goes unmentioned, so nothing here has to tell
24//! OOXML from a bare archive, which is the problem that made the sniffing
25//! design collapse in the first place.
26
27/// What the bytes are, where they are something that runs.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Executable {
31    /// `MZ`. A Windows executable image: `.exe`, `.dll`, and the rest.
32    Pe,
33    /// `\x7fELF`. A Linux or BSD executable or shared object.
34    Elf,
35    /// A Mach-O image, in either byte order and either width.
36    MachO,
37    /// `#!`. A script naming its own interpreter, which is what makes it run.
38    Script,
39}
40
41impl Executable {
42    /// What to call it in the sentence shown to a person.
43    #[must_use]
44    pub fn describes(self) -> &'static str {
45        match self {
46            Self::Pe => "a Windows executable",
47            Self::Elf => "a Linux executable",
48            Self::MachO => "a macOS executable",
49            Self::Script => "a script",
50        }
51    }
52}
53
54/// How many bytes of a content file this needs. Four for every magic number
55/// here, and two for a shebang.
56pub const HEAD: usize = 4;
57
58/// What the leading bytes are, where they are something that runs.
59///
60/// **`cafebabe` is missing on purpose.** It is a Mach-O universal binary and it
61/// is also a Java class file, and telling them apart means reading the field
62/// after it and deciding whether it is an architecture count or a version. A
63/// false positive here is a warning shown to somebody about a content file
64/// that is fine, which costs more than missing a fat binary — and a fat binary's
65/// members are Mach-O, so the single-architecture form is the common one and is
66/// caught.
67#[must_use]
68pub fn executable(head: &[u8]) -> Option<Executable> {
69    match head {
70        [b'M', b'Z', ..] => Some(Executable::Pe),
71        [0x7f, b'E', b'L', b'F', ..] => Some(Executable::Elf),
72        // Thin Mach-O. The last byte is the width — `ce` for 32-bit, `cf` for
73        // 64 — and the two arms are the two byte orders it can be written in.
74        [0xfe, 0xed, 0xfa, 0xce | 0xcf, ..] | [0xce | 0xcf, 0xfa, 0xed, 0xfe, ..] => {
75            Some(Executable::MachO)
76        }
77        [b'#', b'!', ..] => Some(Executable::Script),
78        _ => None,
79    }
80}
81
82/// Whether the content file is something that runs while its name says
83/// otherwise.
84///
85/// `None` where the bytes are not executable, and `None` where they are and the
86/// extension already says so — a `.exe` that is a PE image is not
87/// misrepresenting itself, whatever policy goes on to decide about it.
88///
89/// The extension is the folded one from [`crate::extension::policy_key`]. An
90/// extension too exotic to fold is not on the list below and so does not
91/// suppress the report, which is the safe direction: the content file is
92/// executable and the name says something nobody can compare.
93#[must_use]
94pub fn misrepresents(head: &[u8], policy_key: Option<&str>) -> Option<Executable> {
95    let what = executable(head)?;
96    match policy_key {
97        Some(k) if EXPECTED.contains(&k) => None,
98        _ => Some(what),
99    }
100}
101
102/// Extensions where executable content is what a person would expect.
103///
104/// Not a type table and not a policy list — nothing is permitted or refused by
105/// being here. It exists so that the warning does not fire on a content file
106/// that is exactly what its name says, and it is short because it only has to
107/// cover the names people actually use for things that run. An extension
108/// missing from it costs a warning shown about an honest content file, which
109/// is the direction to err in.
110const EXPECTED: &[&str] = &[
111    // Windows
112    "exe", "dll", "com", "scr", "sys", "cpl", "ocx", "drv", "efi", // Unix
113    "so", "o", "a", "bin", "elf", "ko", // macOS
114    "dylib", "bundle", // scripts, for the shebang arm
115    "sh", "bash", "zsh", "csh", "ksh", "fish", "py", "pl", "rb", "lua", "tcl", "awk", "sed", "r",
116    "ps1",
117];
118
119#[cfg(test)]
120mod tests {
121    use super::{executable, misrepresents, Executable};
122
123    #[test]
124    fn recognises_the_four_things_that_run() {
125        assert_eq!(executable(b"MZ\x90\x00"), Some(Executable::Pe));
126        assert_eq!(executable(b"\x7fELF"), Some(Executable::Elf));
127        assert_eq!(executable(b"\xcf\xfa\xed\xfe"), Some(Executable::MachO));
128        assert_eq!(executable(b"#!/bin/sh"), Some(Executable::Script));
129    }
130
131    #[test]
132    fn mach_o_is_recognised_in_both_orders_and_both_widths() {
133        for magic in [
134            b"\xfe\xed\xfa\xce",
135            b"\xce\xfa\xed\xfe",
136            b"\xfe\xed\xfa\xcf",
137            b"\xcf\xfa\xed\xfe",
138        ] {
139            assert_eq!(executable(magic), Some(Executable::MachO), "{magic:x?}");
140        }
141    }
142
143    #[test]
144    fn a_universal_binary_is_not_reported() {
145        // `cafebabe` is a Java class file too, and a warning shown about an
146        // honest content file costs more than missing a fat binary whose
147        // members are Mach-O anyway. See the note on `executable`.
148        assert_eq!(executable(b"\xca\xfe\xba\xbe"), None);
149    }
150
151    #[test]
152    fn a_pdf_is_not_something_that_runs() {
153        assert_eq!(executable(b"%PDF"), None);
154    }
155
156    #[test]
157    fn a_zip_is_not_something_that_runs() {
158        // The case that sank the sniffing design: this is a `.docx`, an `.odt`,
159        // a `.jar` and a bare archive, and nothing here has to know which.
160        assert_eq!(executable(b"PK\x03\x04"), None);
161    }
162
163    #[test]
164    fn short_input_answers_rather_than_panicking() {
165        // A zero-length content file is conformant under SPEC 2.3, and a one-byte
166        // one is a slice every pattern here is longer than.
167        assert_eq!(executable(b""), None);
168        assert_eq!(executable(b"M"), None);
169        assert_eq!(executable(b"\x7fEL"), None);
170        // Two bytes are enough for a shebang and not for the rest.
171        assert_eq!(executable(b"#!"), Some(Executable::Script));
172    }
173
174    #[test]
175    fn an_executable_wearing_a_documents_name_is_reported() {
176        assert_eq!(
177            misrepresents(b"MZ\x90\x00", Some("pdf")),
178            Some(Executable::Pe)
179        );
180    }
181
182    #[test]
183    fn an_executable_wearing_its_own_name_is_not() {
184        assert_eq!(misrepresents(b"MZ\x90\x00", Some("exe")), None);
185        assert_eq!(misrepresents(b"\x7fELF", Some("so")), None);
186        assert_eq!(misrepresents(b"#!/bin/sh", Some("sh")), None);
187    }
188
189    #[test]
190    fn a_document_is_never_reported_whatever_it_is_called() {
191        assert_eq!(misrepresents(b"%PDF", Some("pdf")), None);
192        assert_eq!(misrepresents(b"%PDF", Some("exe")), None);
193        assert_eq!(misrepresents(b"PK\x03\x04", Some("docx")), None);
194    }
195
196    #[test]
197    fn an_extension_too_exotic_to_fold_does_not_suppress_the_report() {
198        // `policy_key` answers `None` for one that is not ASCII alphanumeric.
199        // The content file is executable and the name says something nothing
200        // can compare, which is the case to report rather than the case to
201        // excuse.
202        assert_eq!(misrepresents(b"MZ\x90\x00", None), Some(Executable::Pe));
203    }
204}