CVE-2023-34411 (Rust, published 2023-06-05T04:15Z)
Description: value: The xml-rs crate before 0.8.14 for Rust and Crab allows a denial of service (panic) via an invalid <! token (such as <!DOCTYPEs/%<!A nesting) in an XML document. The earliest affected version is 0.8.9.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-611 \u2014 Improper Restriction of XML External Entity Reference
The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
Repository: https://github.com/00xc/xml-rs
Files changed:
README.md
src/reader/lexer.rs
src/reader/parser/inside_cdata.rs
src/reader/parser/inside_doctype.rs
src/reader/parser/inside_processing_instruction.rs
src/reader/parser/mod.rs
src/reader/parser/outside_tag.rs
tests/xmlconf.rs
Commit message: Parse DOCTYPE markup declarations


Vulnerable code:
* DTD validation is not supported, `<!DOCTYPE>` declarations are completely ignored; thus no
    DoctypeFinishing(u8),
    /// Enables error handling so `next_token` will return `Some(Err(..))`
    /// upon invalid lexeme.
    #[inline]
    pub fn enable_errors(&mut self) { self.skip_errors = false; }

    #[inline]
    pub fn disable_errors(&mut self) { self.skip_errors = true; }
            State::InsideComment |
            State::DoctypeFinishing(_) =>
            State::DoctypeFinishing(d)            => self.doctype_finishing(c, d),
            '/'                        => self.move_to_with(State::Normal, Token::ClosingTagStart),
            _ if is_whitespace_char(c) => self.move_to_with_unread(State::Normal, &[c], Token::OpeningTagStart),
            _ if is_name_char(c)       => self.move_to_with_unread(State::Normal, &[c], Token::OpeningTagStart),
            DOCTYP ; 'E' ; "<!DOCTYP" ; self.move_to_with(State::DoctypeFinishing(1), Token::DoctypeStart)
    fn doctype_finishing(&mut self, c: char, d: u8) -> Result {
            '<' => self.move_to(State::DoctypeFinishing(d + 1)),
            '>' if d == 1 => self.move_to_with(State::Normal, Token::TagEnd),
            '>' => self.move_to(State::DoctypeFinishing(d - 1)),
            '>' => self.move_to_with(State::Normal, Token::ProcessingInstructionEnd),
            '>' => self.move_to_with(State::Normal, Token::EmptyTagEnd),
            _ => self.move_to_with_unread(State::Normal, &[c], Token::Character('/')),
                '>' => self.move_to_with(State::Normal, Token::CommentEnd),
                '>' => self.move_to_with(State::Normal, Token::CDataEnd),
            r#"<a><!DOCTYPE ab[<!ELEMENT ba> ]> "#
        // eof_check!("--"       ; 0, 2);
                self.lexer.enable_errors();
                self.lexer.enable_errors();
                    self.lexer.enable_errors();
                        self.lexer.disable_errors();
        "rmt-e2e-24", // Either the built-in entity or a character reference can be used to represent greater-than after two close-square-brackets
        "o-p43pass1", // Valid use of character data, comments, processing instructions and CDATA sections within the start and end tag.
        "o-p68pass1", // Valid entity references.  Also ensures that a charref to           '&' isn't interpreted as an entity reference open delimiter
        "o-p04pass1", // names with all valid ASCII characters, and one from each               other class in NameChar
        "o-p05pass1", // various valid Name constructions
        "o-p09fail4", // quote types must match
        "o-p09fail5", // quote types must match
        "o-p11fail1", // quote types must match
        "o-p11fail2", // cannot contain delimiting quotes
        "o-p12fail1", // '"' excluded
        "o-p29fail1", // A processor must not pass unknown declaration types.
        "o-p45fail1", // ELEMENT must be upper case.
        "o-p71fail3", // no S after "<!"
        "nwf-dtd00", // Comma mandatory in content model
        "nwf-dtd01", // Can't mix comma and vertical bar in content models
        "not-wf-sa-063", // Conditional sections may only appear in the external     DTD subset.
        "not-wf-sa-107", // Invalid document type declaration.  CDATA alone is invalid.
        "not-wf-sa-179", // Invalid syntax matching double quote is missing.

Fixed code:
* There is only very rudimentary parsing of `<!DOCTYPE>` declarations; thus no
* DTD validation is not supported;
    /// `<!` of `ENTITY`
    MarkupDeclarationStart,
#[derive(Copy, Clone)]
    /// Other items like `<!ELEMENT` in DTD
    InsideMarkupDeclaration,
    InsideDoctype,
    /// `<!ENTITY "here">`
    InsideMarkupDeclarationQuotedString(QuoteStyle),
}

#[derive(Copy, Clone, Eq, PartialEq)]
enum QuoteStyle {
    Single, Double
    /// Default state to go back to after a tag end (may be `InsideDoctype`)
    normal_state: State,
            normal_state: State::Normal,
    pub(crate) fn disable_errors(&mut self) { self.skip_errors = true; }
            State::InsideComment | State::InsideMarkupDeclaration |
            State::InsideDoctype | State::InsideMarkupDeclarationQuotedString(_) =>
            State::InsideDoctype                  => self.inside_doctype(c),
            State::InsideMarkupDeclaration       => self.markup_declaration(c),
            State::InsideMarkupDeclarationQuotedString(q) => self.markup_declaration_string(c, q),
    #[inline]
    fn move_to_and_reset_normal(&mut self, st: State, token: Token) -> Result {
        self.normal_state = st;
        self.st = st;
        Ok(Some(token))
    }

        // These tokens are used by `<?xml?>` parser
            '/'                        => self.move_to_with(self.normal_state, Token::ClosingTagStart),
            _ if is_whitespace_char(c) => self.move_to_with_unread(self.normal_state, &[c], Token::OpeningTagStart),
            _ if is_name_char(c)       => self.move_to_with_unread(self.normal_state, &[c], Token::OpeningTagStart),
            'E' | 'A' | 'N' if matches!(self.normal_state, State::InsideDoctype) => self.move_to_with(State::InsideMarkupDeclaration, Token::MarkupDeclarationStart),
    /// Encountered '<!…' that isn't DOCTYPE or CDATA
    fn markup_declaration(&mut self, c: char) -> Result {
        match c {
            '<'                        => self.handle_error("<!", c),
            '>'                        => self.move_to_with(self.normal_state, Token::TagEnd),
            '&'                        => Ok(Some(Token::ReferenceStart)),
            ';'                        => Ok(Some(Token::ReferenceEnd)),
            '"'                        => self.move_to_with(State::InsideMarkupDeclarationQuotedString(QuoteStyle::Double), Token::DoubleQuote),
            '\''                       => self.move_to_with(State::InsideMarkupDeclarationQuotedString(QuoteStyle::Single), Token::SingleQuote),
            _ => Ok(None),
        }
    }

    fn markup_declaration_string(&mut self, c: char, q: QuoteStyle) -> Result {
        match c {
            '"' if q == QuoteStyle::Double  => self.move_to_with(State::InsideMarkupDeclaration, Token::DoubleQuote),
            '\'' if q == QuoteStyle::Single => self.move_to_with(State::InsideMarkupDeclaration, Token::SingleQuote),
            _ => Ok(None),
        }
    }

            DOCTYP ; 'E' ; "<!DOCTYP" ; self.move_to_and_reset_normal(State::InsideDoctype, Token::DoctypeStart)
    fn inside_doctype(&mut self, c: char) -> Result {
            '>' => self.move_to_and_reset_normal(State::Normal, Token::TagEnd),
            '<'                        => self.move_to(State::TagStarted),
            '&'                        => Ok(Some(Token::ReferenceStart)),
            ';'                        => Ok(Some(Token::ReferenceEnd)),
            '>' => self.move_to_with(self.normal_state, Token::ProcessingInstructionEnd),
            '>' => self.move_to_with(self.normal_state, Token::EmptyTagEnd),
            _ => self.move_to_with_unread(self.normal_state, &[c], Token::Character('/')),
                '>' => self.move_to_with(self.normal_state, Token::CommentEnd),
                '>' => self.move_to_with(self.normal_state, Token::CDataEnd),
            r#"<a><!DOCTYPE ab[<!ELEMENT ba ">>>>>"> ]> "#
            Token::MarkupDeclarationStart
            Token::DoubleQuote
            Token::DoubleQuote
            Token::TagEnd
    #[test]
    fn doctype_internal_pi_comment() {
        let (mut lex, mut buf) = make_lex_and_buf(
            "<!DOCTYPE a [\n<!ELEMENT leopard ANY> <!-- <?non?>--> <?pi > ?> \n]>"
        );
        assert_oks!(for lex and buf ;
            Token::DoctypeStart
            Token::MarkupDeclarationStart
            Token::TagEnd
            Token::CommentStart
            Token::Whitespace(' ')
            Token::Character('<')
            Token::Character('?')
            Token::Character('n')
            Token::Character('o')
            Token::Character('n')
            Token::Character('?')
            Token::Character('>')
            Token::CommentEnd
            Token::ProcessingInstructionStart
            Token::Character('p')
            Token::Character('i')
            Token::Whitespace(' ')
            Token::TagEnd // not really
            Token::Whitespace(' ')
            Token::ProcessingInstructionEnd
            Token::TagEnd // DTD
        );
        assert_none!(for lex and buf);
    }

            Token::MarkupDeclarationStart => {
                self.into_state_continue(State::InsideDoctypeMarkupDeclaration)
            },

            _ => None,
        }
    }

    pub fn inside_doctype_markup_declaration(&mut self, t: Token) -> Option<Result> {
        match t {
            Token::TagEnd => {
                self.into_state_continue(State::InsideDoctype)
            }

    InsideDoctypeMarkupDeclaration,
            State::InsideDoctypeMarkupDeclaration => self.inside_doctype_markup_declaration(t),
        "o-p04pass1", // names with all valid ASCII characters, and one from each               other class in NameChar
        "o-p05pass1", // various valid Name constructions
        "o-p11pass1", // p11pass1.xml       system literals may not contain     URI fragments
        "o-p12fail1", // p12fail1.xml       '"' excluded
        "o-p12fail4", // p12fail4.xml       '>' excluded
        "o-p12fail5", // p12fail5.xml       '<' excluded
        "o-p43pass1", // Valid use of character data, comments, processing instructions and CDATA sections within the start and end tag.
        "o-p68pass1", // Valid entity references.  Also ensures that a charref to           '&' isn't interpreted as an entity reference open delimiter
        "o-p76fail4", // p76fail4.xml       notation names are Names
        "nwf-dtd00", // Comma mandatory in content model
        "nwf-dtd01", // Can't mix comma and vertical bar in content models
        "pi", // pi.xml      No space between PI target name and data