Skip to main content

orinium_browser/engine/
permissions.rs

1//! Capability set inherited down the frame tree.
2//!
3//! A browsing context never decides its own privileges from its URL alone.
4//! Instead its effective [`Permissions`] are the intersection of the base
5//! capabilities of its own origin and the effective permissions of its parent
6//! frame ([`Permissions::inherit`]). By induction a descendant can never hold a
7//! capability its top-level ancestor lacks, so a nested or embedded context
8//! cannot escalate beyond the root document.
9//!
10//! Only browser-created chrome origins hold [`Permissions::chrome`] (which
11//! includes arbitrary local-file access); ordinary documents, including bundled
12//! `resource:///` pages, only ever get [`Permissions::content_for`].
13
14use url::Url;
15
16/// The capabilities a document is allowed to exercise.
17///
18/// Scheme capabilities (`network`/`resource`/`file`) govern *what can be
19/// loaded*; they never grant the right to read a cross-origin response. That
20/// latter authority is separate: only `privileged` (a browser-created chrome
21/// context) bypasses CORS. Keeping the two independent means a capability can
22/// never be mistaken for a CORS grant, nor vice versa.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub struct Permissions {
25    /// May load network-backed schemes (`http`, `https`) and `data:` URLs.
26    pub network: bool,
27    /// May load `resource://` content. The resource loader confines such loads
28    /// to the bundle directory.
29    pub resource: bool,
30    /// May load arbitrary local files via `file://`. Reserved for chrome.
31    pub file: bool,
32    /// Marks a browser-created chrome context (shell, DevTools). This is the
33    /// only authority that bypasses ordinary CORS read checks; it is tracked
34    /// separately from the scheme capabilities so the two mechanisms stay
35    /// independent.
36    pub privileged: bool,
37}
38
39impl Permissions {
40    /// No capabilities at all.
41    pub const fn none() -> Self {
42        Self {
43            network: false,
44            resource: false,
45            file: false,
46            privileged: false,
47        }
48    }
49
50    /// Base capabilities for a document loaded from `url`.
51    ///
52    /// Every document may use the network; `resource://` documents additionally
53    /// reach bundled resources (confined to the bundle root). Local files are
54    /// never reachable from content, and ordinary documents are never
55    /// privileged.
56    pub fn content_for(url: &Url) -> Self {
57        Self {
58            network: true,
59            resource: url.scheme() == "resource",
60            file: false,
61            privileged: false,
62        }
63    }
64
65    /// Base capabilities for a document loaded from the string form of a URL.
66    ///
67    /// Unparsable URLs are treated as network-capable opaque content, matching a
68    /// document that cannot be resolved to a bundled or local origin.
69    pub fn content_for_str(url: &str) -> Self {
70        match Url::parse(url) {
71            Ok(url) => Self::content_for(&url),
72            Err(_) => Self {
73                network: true,
74                resource: false,
75                file: false,
76                privileged: false,
77            },
78        }
79    }
80
81    /// Full capabilities for browser-created origins (shell, DevTools).
82    pub const fn chrome() -> Self {
83        Self {
84            network: true,
85            resource: true,
86            file: true,
87            privileged: true,
88        }
89    }
90
91    /// Derives the permissions a child frame may hold, given its parent's
92    /// effective permissions. The result is the intersection of the two, so a
93    /// child can never exceed any ancestor.
94    pub const fn inherit(self, parent: Self) -> Self {
95        Self {
96            network: self.network && parent.network,
97            resource: self.resource && parent.resource,
98            file: self.file && parent.file,
99            privileged: self.privileged && parent.privileged,
100        }
101    }
102
103    /// Effective capabilities for a child frame loaded from `child_url` under
104    /// this (parent) set: the base capabilities of the child's own origin,
105    /// intersected with the parent's. A child can never gain a capability, and
106    /// privilege is never inherited.
107    pub fn for_child_frame(self, child_url: &str) -> Self {
108        Self::content_for_str(child_url).inherit(self)
109    }
110
111    /// Whether this set is a browser-created chrome context: the only authority
112    /// that bypasses ordinary CORS read checks.
113    pub const fn is_chrome(self) -> bool {
114        self.privileged
115    }
116
117    /// Whether a resource addressed by `url` may be loaded under these
118    /// permissions.
119    pub fn allows(self, url: &Url) -> bool {
120        match url.scheme() {
121            "http" | "https" | "data" => self.network,
122            "resource" => self.resource,
123            "file" => self.file,
124            _ => false,
125        }
126    }
127
128    /// Whether a resource addressed by the string form of a URL may be loaded.
129    ///
130    /// Unparsable URLs are treated as opaque content and only allowed when the
131    /// network capability is held.
132    pub fn allows_str(self, url: &str) -> bool {
133        match Url::parse(url) {
134            Ok(url) => self.allows(&url),
135            Err(_) => self.network,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::test_support::parse_url as url;
144
145    #[test]
146    fn content_resource_page_gets_no_file_access() {
147        let perms = Permissions::content_for(&url("resource:///test/index.html"));
148        assert!(perms.network);
149        assert!(perms.resource);
150        assert!(!perms.file);
151        assert!(!perms.allows(&url("file:///etc/passwd")));
152        assert!(perms.allows(&url("resource:///test/style.css")));
153    }
154
155    #[test]
156    fn web_page_reaches_network_only() {
157        let perms = Permissions::content_for(&url("https://example.test/"));
158        assert!(perms.allows(&url("https://example.test/x.js")));
159        assert!(perms.allows(&url("data:text/plain,hi")));
160        assert!(!perms.allows(&url("resource:///test/index.html")));
161        assert!(!perms.allows(&url("file:///etc/passwd")));
162    }
163
164    #[test]
165    fn chrome_reaches_files() {
166        let chrome = Permissions::chrome();
167        assert!(chrome.is_chrome());
168        assert!(chrome.privileged);
169        assert!(chrome.allows(&url("file:///etc/passwd")));
170        assert!(chrome.allows(&url("resource:///test/index.html")));
171    }
172
173    #[test]
174    fn privilege_is_separate_from_scheme_capabilities() {
175        // Every scheme capability can be present without any CORS-bypass
176        // authority: privilege is not implied by reaching files or resources.
177        let permissive_content = Permissions {
178            network: true,
179            resource: true,
180            file: true,
181            privileged: false,
182        };
183        assert!(!permissive_content.is_chrome());
184        assert!(permissive_content.allows(&url("file:///etc/passwd")));
185    }
186
187    #[test]
188    fn inherit_never_grants_more_than_the_parent() {
189        // A chrome frame embedding a web frame cannot hand down file access or
190        // privilege.
191        let child =
192            Permissions::content_for(&url("https://example.test/")).inherit(Permissions::chrome());
193        assert!(!child.file);
194        assert!(!child.resource);
195        assert!(!child.privileged);
196        assert!(child.network);
197
198        // A web frame embedding a chrome frame also yields web-only.
199        let child =
200            Permissions::chrome().inherit(Permissions::content_for(&url("https://example.test/")));
201        assert!(!child.file);
202        assert!(!child.privileged);
203    }
204
205    #[test]
206    fn privilege_never_reappears_down_the_tree() {
207        // chrome -> web -> chrome: once privilege is lost it cannot return,
208        // even if the deepest frame is itself a chrome URL.
209        let web = Permissions::chrome().for_child_frame("https://example.test/");
210        let regressed = web.for_child_frame("resource:///shell.html");
211        assert!(!regressed.privileged);
212        assert!(!regressed.is_chrome());
213    }
214
215    #[test]
216    fn for_child_frame_uses_the_child_origin_not_the_parent() {
217        // A resource child of a chrome parent keeps bundle access but is not
218        // privileged; a web child of a chrome parent loses resource access too.
219        let chrome = Permissions::chrome();
220        let resource_child = chrome.for_child_frame("resource:///test/index.html");
221        assert!(resource_child.resource);
222        assert!(!resource_child.file);
223        assert!(!resource_child.privileged);
224
225        let web_child = chrome.for_child_frame("https://example.test/");
226        assert!(!web_child.resource);
227        assert!(!web_child.file);
228        assert!(!web_child.privileged);
229
230        // An unparsable child URL is treated as opaque network content.
231        let opaque_child = chrome.for_child_frame("not a url");
232        assert!(opaque_child.network);
233        assert!(!opaque_child.resource);
234        assert!(!opaque_child.file);
235        assert!(!opaque_child.privileged);
236    }
237
238    #[test]
239    fn inherit_of_chrome_and_chrome_stays_chrome() {
240        assert_eq!(
241            Permissions::chrome().inherit(Permissions::chrome()),
242            Permissions::chrome()
243        );
244    }
245
246    #[test]
247    fn inherit_is_idempotent() {
248        let perms = Permissions::content_for(&url("resource:///a/b.html"));
249        assert_eq!(perms.inherit(perms), perms);
250    }
251}