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 payload whose bytes
12//! are an executable image or a script under a name that claims neither — the
13//! 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 payload that is exactly
19//! what it claims and still hostile passes without comment, and that is not a
20//! 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 payload this needs. Four for every magic number here,
55/// 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 payload that is
64/// 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 payload is something that runs while its name says otherwise.
83///
84/// `None` where the bytes are not executable, and `None` where they are and the
85/// extension already says so — a `.exe` that is a PE image is not
86/// misrepresenting itself, whatever policy goes on to decide about it.
87///
88/// The extension is the folded one from [`crate::extension::policy_key`]. An
89/// extension too exotic to fold is not on the list below and so does not
90/// suppress the report, which is the safe direction: the payload is executable
91/// and the name says something nobody can compare.
92#[must_use]
93pub fn misrepresents(head: &[u8], policy_key: Option<&str>) -> Option<Executable> {
94    let what = executable(head)?;
95    match policy_key {
96        Some(k) if EXPECTED.contains(&k) => None,
97        _ => Some(what),
98    }
99}
100
101/// Extensions where executable content is what a person would expect.
102///
103/// Not a type table and not a policy list — nothing is permitted or refused by
104/// being here. It exists so that the warning does not fire on a payload that is
105/// exactly what its name says, and it is short because it only has to cover the
106/// names people actually use for things that run. An extension missing from it
107/// costs a warning shown about an honest payload, which is the direction to err
108/// in.
109const EXPECTED: &[&str] = &[
110    // Windows
111    "exe", "dll", "com", "scr", "sys", "cpl", "ocx", "drv", "efi", // Unix
112    "so", "o", "a", "bin", "elf", "ko", // macOS
113    "dylib", "bundle", // scripts, for the shebang arm
114    "sh", "bash", "zsh", "csh", "ksh", "fish", "py", "pl", "rb", "lua", "tcl", "awk", "sed", "r",
115    "ps1",
116];
117
118#[cfg(test)]
119mod tests {
120    use super::{executable, misrepresents, Executable};
121
122    #[test]
123    fn recognises_the_four_things_that_run() {
124        assert_eq!(executable(b"MZ\x90\x00"), Some(Executable::Pe));
125        assert_eq!(executable(b"\x7fELF"), Some(Executable::Elf));
126        assert_eq!(executable(b"\xcf\xfa\xed\xfe"), Some(Executable::MachO));
127        assert_eq!(executable(b"#!/bin/sh"), Some(Executable::Script));
128    }
129
130    #[test]
131    fn mach_o_is_recognised_in_both_orders_and_both_widths() {
132        for magic in [
133            b"\xfe\xed\xfa\xce",
134            b"\xce\xfa\xed\xfe",
135            b"\xfe\xed\xfa\xcf",
136            b"\xcf\xfa\xed\xfe",
137        ] {
138            assert_eq!(executable(magic), Some(Executable::MachO), "{magic:x?}");
139        }
140    }
141
142    #[test]
143    fn a_universal_binary_is_not_reported() {
144        // `cafebabe` is a Java class file too, and a warning shown about an
145        // honest payload costs more than missing a fat binary whose members
146        // are Mach-O anyway. See the note on `executable`.
147        assert_eq!(executable(b"\xca\xfe\xba\xbe"), None);
148    }
149
150    #[test]
151    fn a_pdf_is_not_something_that_runs() {
152        assert_eq!(executable(b"%PDF"), None);
153    }
154
155    #[test]
156    fn a_zip_is_not_something_that_runs() {
157        // The case that sank the sniffing design: this is a `.docx`, an `.odt`,
158        // a `.jar` and a bare archive, and nothing here has to know which.
159        assert_eq!(executable(b"PK\x03\x04"), None);
160    }
161
162    #[test]
163    fn short_input_answers_rather_than_panicking() {
164        // A zero-length payload is conformant under SPEC 2.3, and a one-byte
165        // one is a slice every pattern here is longer than.
166        assert_eq!(executable(b""), None);
167        assert_eq!(executable(b"M"), None);
168        assert_eq!(executable(b"\x7fEL"), None);
169        // Two bytes are enough for a shebang and not for the rest.
170        assert_eq!(executable(b"#!"), Some(Executable::Script));
171    }
172
173    #[test]
174    fn an_executable_wearing_a_documents_name_is_reported() {
175        assert_eq!(
176            misrepresents(b"MZ\x90\x00", Some("pdf")),
177            Some(Executable::Pe)
178        );
179    }
180
181    #[test]
182    fn an_executable_wearing_its_own_name_is_not() {
183        assert_eq!(misrepresents(b"MZ\x90\x00", Some("exe")), None);
184        assert_eq!(misrepresents(b"\x7fELF", Some("so")), None);
185        assert_eq!(misrepresents(b"#!/bin/sh", Some("sh")), None);
186    }
187
188    #[test]
189    fn a_document_is_never_reported_whatever_it_is_called() {
190        assert_eq!(misrepresents(b"%PDF", Some("pdf")), None);
191        assert_eq!(misrepresents(b"%PDF", Some("exe")), None);
192        assert_eq!(misrepresents(b"PK\x03\x04", Some("docx")), None);
193    }
194
195    #[test]
196    fn an_extension_too_exotic_to_fold_does_not_suppress_the_report() {
197        // `policy_key` answers `None` for one that is not ASCII alphanumeric.
198        // The payload is executable and the name says something nothing can
199        // compare, which is the case to report rather than the case to excuse.
200        assert_eq!(misrepresents(b"MZ\x90\x00", None), Some(Executable::Pe));
201    }
202}