Skip to main content

libxml_rs/xslt/security/
mod.rs

1//! XSLT security preferences (§33, §85 Phase 8; R-000125 closure).
2//!
3//! Implements the `xsltSecurityPrefs` API for controlling what operations are
4//! permitted during XSLT transformations.
5//!
6//! # Upstream mapping
7//!
8//! | Function | Header |
9//! |---|---|
10//! | `xsltNewSecurityPrefs` | libxslt/security.h |
11//! | `xsltFreeSecurityPrefs` | libxslt/security.h |
12//! | `xsltSetSecurityPrefs` | libxslt/security.h |
13//! | `xsltGetSecurityPrefs` | libxslt/security.h |
14//! | `xsltSetDefaultSecurityPrefs` | libxslt/security.h |
15//! | `xsltGetDefaultSecurityPrefs` | libxslt/security.h |
16//!
17//! # UPSTREAM-PARITY (R-000125)
18//!
19//! The security model is **callback-based**, not value-based. Upstream
20//! `xsltSecurityPrefs` holds five `xsltSecurityCheck` function pointers
21//! (readFile / createFile / createDir / readNet / writeNet) and:
22//!
23//! - `xsltNewSecurityPrefs()` returns a **zeroed** block (all callbacks NULL);
24//! - `xsltSetSecurityPrefs(sec, option, func)` stores the callback, with the
25//!   upstream quirk that `XSLT_SECPREF_WRITE_FILE` writes the **createFile**
26//!   slot (`security.c` `xsltSetSecurityPrefs` case);
27//! - `xsltGetSecurityPrefs(sec, option)` returns the stored callback or NULL;
28//! - the callbacks are never invoked by libxslt 1.1.42 itself — the surface is
29//!   registration-only, exercised by consumers such as `xsltproc --nowrite`
30//!   which registers `xsltSecurityForbid`.
31//!
32//! Earlier revisions of this module implemented a divergent int allow/deny
33//! model (`xsltSetSecurityPrefs(sec, option, value: c_int)`); the module was
34//! reimplemented to the upstream contract (R-000125, closed 11.1-G/H).
35
36#![allow(
37    clippy::missing_inline_in_public_items,
38    clippy::must_use_candidate,
39    clippy::missing_safety_doc
40)]
41
42use crate::abi::structs::*;
43use crate::abi::types::*;
44use std::ffi::{c_char, c_void};
45use std::os::raw::c_int;
46use std::ptr;
47use std::sync::Mutex;
48
49/// Security option: read a file from the filesystem.
50pub const XSLT_SECPREF_READ_FILE: c_int = 1;
51
52/// Security option: write a file to the filesystem.
53pub const XSLT_SECPREF_WRITE_FILE: c_int = 2;
54
55/// Security option: create a directory on the filesystem.
56pub const XSLT_SECPREF_CREATE_DIRECTORY: c_int = 3;
57
58/// Security option: read a network resource.
59pub const XSLT_SECPREF_READ_NETWORK: c_int = 4;
60
61/// Security option: write to a network resource.
62pub const XSLT_SECPREF_WRITE_NETWORK: c_int = 5;
63
64/// Security check callback (upstream `xsltSecurityCheck`):
65/// `int (*)(xsltSecurityPrefsPtr sec, xsltTransformContextPtr ctxt,
66///           const char *value)` — returns non-zero to allow, 0 to deny.
67pub type xsltSecurityCheck = unsafe extern "C" fn(*mut c_void, *mut c_void, *const c_char) -> c_int;
68
69/// Internal security preferences structure — five check callbacks, one per
70/// controllable option (upstream `xsltSecurityPrefs` in security.c).
71///
72/// # ABI
73/// The structure is private in upstream (defined only in security.c); the
74/// Rust representation is opaque to C consumers, so the layout is internal.
75#[repr(C)]
76pub struct XsltSecurityPrefs {
77    pub(crate) readFile: Option<xsltSecurityCheck>,
78    pub(crate) createFile: Option<xsltSecurityCheck>,
79    pub(crate) createDir: Option<xsltSecurityCheck>,
80    pub(crate) readNet: Option<xsltSecurityCheck>,
81    pub(crate) writeNet: Option<xsltSecurityCheck>,
82}
83
84/// Wrapper around `*mut c_void` that implements `Send` so it can be stored
85/// in a `Mutex`.
86///
87/// # Safety
88///
89/// The caller is responsible for ensuring that the pointed-to value is
90/// accessed in a thread-safe manner. The global default is only ever written
91/// or read through the `Mutex` guard, so concurrent access is serialized.
92#[repr(transparent)]
93struct SecurityPrefsPtr(*mut c_void);
94
95// SAFETY: Access to the wrapped pointer is serialized via Mutex, making
96// it safe to send between threads.
97unsafe impl Send for SecurityPrefsPtr {}
98
99/// Global default security preferences, stored as a raw pointer behind a
100/// [`Mutex`] for thread-safe access.
101static DEFAULT_SECURITY_PREFS: Mutex<Option<SecurityPrefsPtr>> = Mutex::new(None);
102
103/// Create new security preferences with the upstream default: a **zeroed**
104/// block whose five check callbacks are all NULL.
105///
106/// # Returns
107///
108/// A non-null pointer to the newly allocated security preferences on success.
109/// The caller is responsible for freeing it with [`xsltFreeSecurityPrefs`].
110///
111/// # Safety
112///
113/// The returned pointer must eventually be freed with
114/// [`xsltFreeSecurityPrefs`] to avoid memory leaks.
115#[no_mangle]
116pub unsafe extern "C" fn xsltNewSecurityPrefs() -> *mut c_void {
117    let prefs = Box::new(XsltSecurityPrefs {
118        readFile: None,
119        createFile: None,
120        createDir: None,
121        readNet: None,
122        writeNet: None,
123    });
124    Box::into_raw(prefs) as *mut c_void
125}
126
127/// Free security preferences previously allocated by [`xsltNewSecurityPrefs`].
128///
129/// # Safety
130///
131/// - `sec` must be a pointer returned by [`xsltNewSecurityPrefs`] that has
132///   not yet been freed.
133/// - After this call, `sec` is dangling and must not be dereferenced.
134#[no_mangle]
135pub unsafe extern "C" fn xsltFreeSecurityPrefs(sec: *mut c_void) {
136    if !sec.is_null() {
137        let _ = Box::from_raw(sec as *mut XsltSecurityPrefs);
138    }
139}
140
141/// Update a security option to use the given check callback.
142///
143/// UPSTREAM-PARITY: mirrors `security.c` `xsltSetSecurityPrefs`, including
144/// the quirk that `XSLT_SECPREF_WRITE_FILE` stores into the `createFile`
145/// slot. `option` uses the `xsltSecurityOption` enum values (1-5).
146///
147/// # Returns
148///
149/// 0 on success, -1 if `sec` is NULL or `option` is out of range.
150///
151/// # Safety
152///
153/// `sec` must point to a valid [`XsltSecurityPrefs`] obtained from
154/// [`xsltNewSecurityPrefs`] that has not yet been freed.
155#[no_mangle]
156pub unsafe extern "C" fn xsltSetSecurityPrefs(
157    sec: *mut c_void,
158    option: c_int,
159    func: Option<xsltSecurityCheck>,
160) -> c_int {
161    if sec.is_null() {
162        return -1;
163    }
164    let prefs = &mut *(sec as *mut XsltSecurityPrefs);
165    match option {
166        XSLT_SECPREF_READ_FILE => prefs.readFile = func,
167        // UPSTREAM-PARITY: WRITE_FILE writes createFile (upstream quirk).
168        XSLT_SECPREF_WRITE_FILE => prefs.createFile = func,
169        XSLT_SECPREF_CREATE_DIRECTORY => prefs.createDir = func,
170        XSLT_SECPREF_READ_NETWORK => prefs.readNet = func,
171        XSLT_SECPREF_WRITE_NETWORK => prefs.writeNet = func,
172        _ => return -1,
173    }
174    0
175}
176
177/// Look up the check callback configured for a security option.
178///
179/// # Returns
180///
181/// The stored callback, or NULL if `sec` is NULL or `option` is invalid or
182/// unset (upstream `xsltGetSecurityPrefs` returns NULL in those cases).
183///
184/// # Safety
185///
186/// If `sec` is non-null, it must point to a valid [`XsltSecurityPrefs`]
187/// obtained from [`xsltNewSecurityPrefs`] that has not yet been freed.
188#[no_mangle]
189pub unsafe extern "C" fn xsltGetSecurityPrefs(
190    sec: *mut c_void,
191    option: c_int,
192) -> Option<xsltSecurityCheck> {
193    if sec.is_null() {
194        return None;
195    }
196    let prefs = &*(sec as *mut XsltSecurityPrefs);
197    match option {
198        XSLT_SECPREF_READ_FILE => prefs.readFile,
199        XSLT_SECPREF_WRITE_FILE => prefs.createFile,
200        XSLT_SECPREF_CREATE_DIRECTORY => prefs.createDir,
201        XSLT_SECPREF_READ_NETWORK => prefs.readNet,
202        XSLT_SECPREF_WRITE_NETWORK => prefs.writeNet,
203        _ => None,
204    }
205}
206
207/// Set the default security preferences used by new transformations.
208///
209/// The provided pointer is stored as the global default. It is the caller's
210/// responsibility to manage the lifetime of the pointed-to preferences.
211///
212/// # Safety
213///
214/// `sec` must point to a valid [`XsltSecurityPrefs`] that remains valid for
215/// the duration it is set as the default (i.e., until replaced by another
216/// call to this function).
217#[no_mangle]
218pub unsafe extern "C" fn xsltSetDefaultSecurityPrefs(sec: *mut c_void) {
219    let mut guard = DEFAULT_SECURITY_PREFS.lock().unwrap();
220    *guard = Some(SecurityPrefsPtr(sec));
221}
222
223/// Get the current default security preferences.
224///
225/// # Returns
226///
227/// A pointer to the default security preferences previously set with
228/// [`xsltSetDefaultSecurityPrefs`], or NULL if none have been set.
229///
230/// # Safety
231///
232/// The returned pointer is only valid as long as no other call to
233/// [`xsltSetDefaultSecurityPrefs`] has replaced it and the original
234/// [`XsltSecurityPrefs`] has not been freed.
235#[no_mangle]
236pub unsafe extern "C" fn xsltGetDefaultSecurityPrefs() -> *mut c_void {
237    let guard = DEFAULT_SECURITY_PREFS.lock().unwrap();
238    guard.as_ref().map_or(ptr::null_mut(), |p| p.0)
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    /// The security API surface matches the upstream callback contract:
246    /// set/get round-trip callbacks and default to NULL.
247    #[test]
248    fn test_set_get_callback_roundtrip() {
249        unsafe {
250            let prefs = xsltNewSecurityPrefs();
251            assert!(!prefs.is_null());
252            // Freshly created prefs have no callbacks configured.
253            assert!(xsltGetSecurityPrefs(prefs, XSLT_SECPREF_READ_FILE).is_none());
254            assert!(xsltGetSecurityPrefs(prefs, XSLT_SECPREF_WRITE_FILE).is_none());
255            assert!(xsltGetSecurityPrefs(prefs, XSLT_SECPREF_CREATE_DIRECTORY).is_none());
256            assert!(xsltGetSecurityPrefs(prefs, XSLT_SECPREF_READ_NETWORK).is_none());
257            assert!(xsltGetSecurityPrefs(prefs, XSLT_SECPREF_WRITE_NETWORK).is_none());
258            xsltFreeSecurityPrefs(prefs);
259        }
260    }
261
262    /// Registering a callback per option and reading it back works, and the
263    /// upstream WRITE_FILE -> createFile slot quirk is preserved.
264    #[test]
265    fn test_set_get_registered_callback() {
266        unsafe extern "C" fn forbid(
267            _sec: *mut c_void,
268            _ctxt: *mut c_void,
269            _value: *const c_char,
270        ) -> c_int {
271            0
272        }
273        let forbid: xsltSecurityCheck = forbid;
274        unsafe {
275            let prefs = xsltNewSecurityPrefs();
276            assert_eq!(
277                xsltSetSecurityPrefs(prefs, XSLT_SECPREF_READ_FILE, Some(forbid)),
278                0
279            );
280            assert_eq!(
281                xsltSetSecurityPrefs(prefs, XSLT_SECPREF_WRITE_FILE, Some(forbid)),
282                0
283            );
284            assert_eq!(
285                xsltGetSecurityPrefs(prefs, XSLT_SECPREF_READ_FILE),
286                Some(forbid)
287            );
288            // WRITE_FILE reads back from the createFile slot (upstream quirk).
289            assert_eq!(
290                xsltGetSecurityPrefs(prefs, XSLT_SECPREF_WRITE_FILE),
291                Some(forbid)
292            );
293            xsltFreeSecurityPrefs(prefs);
294        }
295    }
296
297    /// Invalid options and NULL prefs return -1 / NULL like upstream.
298    #[test]
299    fn test_invalid_option_and_null() {
300        unsafe {
301            assert_eq!(
302                xsltSetSecurityPrefs(ptr::null_mut(), XSLT_SECPREF_READ_FILE, None),
303                -1
304            );
305            let prefs = xsltNewSecurityPrefs();
306            assert_eq!(xsltSetSecurityPrefs(prefs, 99, None), -1);
307            assert!(xsltGetSecurityPrefs(prefs, 99).is_none());
308            assert!(xsltGetSecurityPrefs(ptr::null_mut(), XSLT_SECPREF_READ_FILE).is_none());
309            xsltFreeSecurityPrefs(prefs);
310        }
311    }
312
313    /// Verify that creating and freeing security prefs works.
314    #[test]
315    fn test_new_free_security_prefs() {
316        unsafe {
317            let prefs = xsltNewSecurityPrefs();
318            assert!(!prefs.is_null());
319            xsltFreeSecurityPrefs(prefs);
320        }
321    }
322
323    /// Verify that freeing a null pointer is a no-op.
324    #[test]
325    fn test_free_null() {
326        unsafe {
327            // Should not panic or crash.
328            xsltFreeSecurityPrefs(ptr::null_mut());
329        }
330    }
331}