oletools_rs 0.1.0

Rust port of oletools — analysis tools for Microsoft Office files (VBA macros, DDE, OLE objects, RTF exploits)
Documentation
//! OOXML Relationships parser.
//!
//! Parses `_rels/.rels` and part-specific `.rels` files to resolve
//! relationships between OOXML parts. Also detects external relationships
//! which may be used for exploitation (e.g., CVE-2022-30190 Follina).

use std::io::Cursor;

use quick_xml::events::Event;
use quick_xml::Reader;

use crate::error::{Error, Result};

/// A single OOXML relationship.
#[derive(Debug, Clone)]
pub struct Relationship {
    /// Relationship ID (e.g., "rId1").
    pub id: String,
    /// Relationship type URI.
    pub rel_type: String,
    /// Target path or URL.
    pub target: String,
    /// Target mode: "Internal" or "External".
    pub target_mode: TargetMode,
}

/// Whether a relationship target is internal or external.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetMode {
    Internal,
    External,
}

impl std::fmt::Display for TargetMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TargetMode::Internal => write!(f, "Internal"),
            TargetMode::External => write!(f, "External"),
        }
    }
}

/// Parse relationships from a `.rels` XML file.
pub fn parse_relationships(xml_data: &[u8]) -> Result<Vec<Relationship>> {
    let mut reader = Reader::from_reader(Cursor::new(xml_data));
    reader.config_mut().trim_text(true);

    let mut relationships = Vec::new();
    let mut buf = Vec::new();

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
                let local_name =
                    String::from_utf8_lossy(e.local_name().as_ref()).to_string();

                if local_name == "Relationship" {
                    let mut id = String::new();
                    let mut rel_type = String::new();
                    let mut target = String::new();
                    let mut target_mode = TargetMode::Internal;

                    for attr in e.attributes().flatten() {
                        let key =
                            String::from_utf8_lossy(attr.key.local_name().as_ref()).to_string();
                        let value = String::from_utf8_lossy(&attr.value).to_string();

                        match key.as_str() {
                            "Id" => id = value,
                            "Type" => rel_type = value,
                            "Target" => target = value,
                            "TargetMode" => {
                                if value.eq_ignore_ascii_case("External") {
                                    target_mode = TargetMode::External;
                                }
                            }
                            _ => {}
                        }
                    }

                    relationships.push(Relationship {
                        id,
                        rel_type,
                        target,
                        target_mode,
                    });
                }
            }
            Ok(Event::Eof) => break,
            Err(e) => {
                return Err(Error::XmlParsing(format!(
                    "Error parsing .rels: {e}"
                )));
            }
            _ => {}
        }
        buf.clear();
    }

    Ok(relationships)
}

/// Check if any relationship is external (potential security risk).
pub fn find_external_relationships(rels: &[Relationship]) -> Vec<&Relationship> {
    rels.iter()
        .filter(|r| r.target_mode == TargetMode::External)
        .collect()
}

/// Well-known relationship type URIs.
pub mod rel_types {
    pub const OFFICE_DOCUMENT: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
    pub const VBA_PROJECT: &str =
        "http://schemas.microsoft.com/office/2006/relationships/vbaProject";
    pub const OLE_OBJECT: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject";
    pub const HYPERLINK: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
    pub const IMAGE: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
    pub const FRAME: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/frame";
    pub const EXTERNAL_LINK: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink";
    pub const ATTACHED_TEMPLATE: &str =
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/attachedTemplate";
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_relationships() {
        let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
    <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://evil.com/payload" TargetMode="External"/>
</Relationships>"#;

        let rels = parse_relationships(xml).unwrap();
        assert_eq!(rels.len(), 2);

        assert_eq!(rels[0].id, "rId1");
        assert_eq!(rels[0].target, "word/document.xml");
        assert_eq!(rels[0].target_mode, TargetMode::Internal);

        assert_eq!(rels[1].id, "rId2");
        assert_eq!(rels[1].target, "https://evil.com/payload");
        assert_eq!(rels[1].target_mode, TargetMode::External);
    }

    #[test]
    fn test_find_external() {
        let xml = br#"<?xml version="1.0"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Id="rId1" Type="test" Target="internal.xml"/>
    <Relationship Id="rId2" Type="test" Target="http://evil.com" TargetMode="External"/>
    <Relationship Id="rId3" Type="test" Target="http://also-evil.com" TargetMode="External"/>
</Relationships>"#;

        let rels = parse_relationships(xml).unwrap();
        let external = find_external_relationships(&rels);
        assert_eq!(external.len(), 2);
    }

    #[test]
    fn test_empty_relationships() {
        let xml = br#"<?xml version="1.0"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>"#;

        let rels = parse_relationships(xml).unwrap();
        assert!(rels.is_empty());
    }

    #[test]
    fn test_target_mode_display() {
        assert_eq!(TargetMode::Internal.to_string(), "Internal");
        assert_eq!(TargetMode::External.to_string(), "External");
    }
}