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}