Commit message: Merge pull request from GHSA-5x45-qp78-g4p4

* Prevent infinite loop in scanning xml content

* Simplify scanning method

* Optimization
CVE: CVE-2022-24763
CWE: CWE-703
Vulnerability description: PJSIP is a free and open source multimedia communication library written in the C language. Versions 2.12 and prior contain a denial-of-service vulnerability that affects PJSIP users that consume PJSIP's XML parsing in their apps. Users are advised to update. There are no known workarounds.

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_parse( pj_pool_t *pool, char *msg, pj_size_t len)
{
    pj_xml_node *node = NULL;
    pj_scanner scanner;
    PJ_USE_EXCEPTION;

    if (!msg || !len || !pool)
	return NULL;

    pj_scan_init( &scanner, msg, len, 
		  PJ_SCAN_AUTOSKIP_WS|PJ_SCAN_AUTOSKIP_NEWLINE, 
		  &on_syntax_error);
    PJ_TRY {
	node =  xml_parse_node(pool, &scanner);
    }
    PJ_CATCH_ANY {
	PJ_LOG(4,(THIS_FILE, "Syntax error parsing XML in line %d column %d",
		  scanner.line, pj_scan_get_col(&scanner)));
    }
    PJ_END;
    pj_scan_fini( &scanner );
    return node;
}

Project: pjproject
File: xml.c

PJ_DEF(int) pj_xml_print(const pj_xml_node *node, char *buf, pj_size_t len,
			 pj_bool_t include_prolog)
{
    int prolog_len = 0;
    int printed;

    if (!node || !buf || !len)
	return 0;

    if (include_prolog) {
	pj_str_t prolog = {"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", 39};
	if ((int)len < prolog.slen)
	    return -1;
	pj_memcpy(buf, prolog.ptr, prolog.slen);
	prolog_len = (int)prolog.slen;
    }

    printed = xml_print_node(node, 0, buf+prolog_len, len-prolog_len) + prolog_len;
    if (printed > 0 && len-printed >= 1) {
	buf[printed++] = '\n';
    }
    return printed;
}

Project: pjproject
File: xml.c

static pj_xml_attr *alloc_attr( pj_pool_t *pool )
{
    return PJ_POOL_ZALLOC_T(pool, pj_xml_attr);
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_clone( pj_pool_t *pool, const pj_xml_node *rhs)
{
    pj_xml_node *node;
    const pj_xml_attr *r_attr;
    const pj_xml_node *child;

    node = alloc_node(pool);

    pj_strdup(pool, &node->name, &rhs->name);
    pj_strdup(pool, &node->content, &rhs->content);

    /* Clone all attributes */
    r_attr = rhs->attr_head.next;
    while (r_attr != &rhs->attr_head) {

	pj_xml_attr *attr;

	attr = alloc_attr(pool);
	pj_strdup(pool, &attr->name, &r_attr->name);
	pj_strdup(pool, &attr->value, &r_attr->value);

	pj_list_push_back(&node->attr_head, attr);

	r_attr = r_attr->next;
    }

    /* Clone all child nodes. */
    child = rhs->node_head.next;
    while (child != (pj_xml_node*) &rhs->node_head) {
	pj_xml_node *new_child;

	new_child = pj_xml_clone(pool, child);
	pj_list_push_back(&node->node_head, new_child);

	child = child->next;
    }

    return node;
}

Project: pjproject
File: xml.c

PJ_DEF(void) pj_xml_add_attr( pj_xml_node *node, pj_xml_attr *attr )
{
    pj_list_push_back(&node->attr_head, attr);
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_find_node_rec(const pj_xml_node *parent, 
					  const pj_str_t *name)
{
    const pj_xml_node *node = parent->node_head.next;

    PJ_CHECK_STACK();

    while (node != (void*)&parent->node_head) {
	pj_xml_node *found;
	if (pj_stricmp(&node->name, name) == 0)
	    return (pj_xml_node*)node;
	found = pj_xml_find_node_rec(node, name);
	if (found)
	    return (pj_xml_node*)found;
	node = node->next;
    }
    return NULL;
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_node_new(pj_pool_t *pool, const pj_str_t *name)
{
    pj_xml_node *node = alloc_node(pool);
    pj_strdup(pool, &node->name, name);
    return node;
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_find_next_node( const pj_xml_node *parent, 
					    const pj_xml_node *node,
					    const pj_str_t *name)
{
    PJ_CHECK_STACK();

    node = node->next;
    while (node != (void*)&parent->node_head) {
	if (pj_stricmp(&node->name, name) == 0)
	    return (pj_xml_node*)node;
	node = node->next;
    }
    return NULL;
}

Project: pjproject
File: xml.c

static void on_syntax_error(struct pj_scanner *scanner)
{
    PJ_UNUSED_ARG(scanner);
    PJ_THROW(EX_SYNTAX_ERROR);
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_find_node(const pj_xml_node *parent, 
				      const pj_str_t *name)
{
    const pj_xml_node *node = parent->node_head.next;

    PJ_CHECK_STACK();

    while (node != (void*)&parent->node_head) {
	if (pj_stricmp(&node->name, name) == 0)
	    return (pj_xml_node*)node;
	node = node->next;
    }
    return NULL;
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_attr*) pj_xml_find_attr( const pj_xml_node *node, 
				       const pj_str_t *name,
				       const pj_str_t *value)
{
    const pj_xml_attr *attr = node->attr_head.next;
    while (attr != (void*)&node->attr_head) {
	if (pj_stricmp(&attr->name, name)==0) {
	    if (value) {
		if (pj_stricmp(&attr->value, value)==0)
		    return (pj_xml_attr*)attr;
	    } else {
		return (pj_xml_attr*)attr;
	    }
	}
	attr = attr->next;
    }
    return NULL;
}

Project: pjproject
File: xml.c

PJ_DEF(void) pj_xml_add_node( pj_xml_node *parent, pj_xml_node *node )
{
    pj_list_push_back(&parent->node_head, node);
}

Project: pjproject
File: xml.c

static int xml_print_node( const pj_xml_node *node, int indent, 
			   char *buf, pj_size_t len )
{
    int i;
    char *p = buf;
    pj_xml_attr *attr;
    pj_xml_node *sub_node;

#define SIZE_LEFT()	((int)(len - (p-buf)))

    PJ_CHECK_STACK();

    /* Print name. */
    if (SIZE_LEFT() < node->name.slen + indent + 5)
	return -1;
    for (i=0; i<indent; ++i)
	*p++ = ' ';
    *p++ = '<';
    pj_memcpy(p, node->name.ptr, node->name.slen);
    p += node->name.slen;

    /* Print attributes. */
    attr = node->attr_head.next;
    while (attr != &node->attr_head) {

	if (SIZE_LEFT() < attr->name.slen + attr->value.slen + 4)
	    return -1;

	*p++ = ' ';

	/* Attribute name. */
	pj_memcpy(p, attr->name.ptr, attr->name.slen);
	p += attr->name.slen;

	/* Attribute value. */
	if (attr->value.slen) {
	    *p++ = '=';
	    *p++ = '"';
	    pj_memcpy(p, attr->value.ptr, attr->value.slen);
	    p += attr->value.slen;
	    *p++ = '"';
	}

	attr = attr->next;
    }

    /* Check for empty node. */
    if (node->content.slen==0 &&
	node->node_head.next==(pj_xml_node*)&node->node_head)
    {
        if (SIZE_LEFT() < 3) return -1;
	*p++ = ' ';
	*p++ = '/';
	*p++ = '>';
	return (int)(p-buf);
    }

    /* Enclosing '>' */
    if (SIZE_LEFT() < 1) return -1;
    *p++ = '>';

    /* Print sub nodes. */
    sub_node = node->node_head.next;
    while (sub_node != (pj_xml_node*)&node->node_head) {
	int printed;

	if (SIZE_LEFT() < indent + 3)
	    return -1;
	//*p++ = '\r';
	*p++ = '\n';

	printed = xml_print_node(sub_node, indent + 1, p, SIZE_LEFT());
	if (printed < 0)
	    return -1;

	p += printed;
	sub_node = sub_node->next;
    }

    /* Content. */
    if (node->content.slen) {
	if (SIZE_LEFT() < node->content.slen) return -1;
	pj_memcpy(p, node->content.ptr, node->content.slen);
	p += node->content.slen;
    }

    /* Enclosing node. */
    if (node->node_head.next != (pj_xml_node*)&node->node_head) {
	if (SIZE_LEFT() < node->name.slen + 5 + indent)
	    return -1;
	//*p++ = '\r';
	*p++ = '\n';
	for (i=0; i<indent; ++i)
	    *p++ = ' ';
    } else {
	if (SIZE_LEFT() < node->name.slen + 3)
	    return -1;
    }
    *p++ = '<';
    *p++ = '/';
    pj_memcpy(p, node->name.ptr, node->name.slen);
    p += node->name.slen;
    *p++ = '>';

#undef SIZE_LEFT

    return (int)(p-buf);
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_find( const pj_xml_node *parent, 
				  const pj_str_t *name,
				  const void *data, 
				  pj_bool_t (*match)(const pj_xml_node *, 
						     const void*))
{
    const pj_xml_node *node = (const pj_xml_node *)parent->node_head.next;

    if (!name && !match)
	return NULL;

    while (node != (const pj_xml_node*) &parent->node_head) {
	if (name) {
	    if (pj_stricmp(&node->name, name)!=0) {
		node = node->next;
		continue;
	    }
	}
	if (match) {
	    if (match(node, data))
		return (pj_xml_node*)node;
	} else {
	    return (pj_xml_node*)node;
	}

	node = node->next;
    }
    return NULL;
}

Project: pjproject
File: xml.c

static pj_xml_node *alloc_node( pj_pool_t *pool )
{
    pj_xml_node *node;

    node = PJ_POOL_ZALLOC_T(pool, pj_xml_node);
    pj_list_init( &node->attr_head );
    pj_list_init( &node->node_head );

    return node;
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_node*) pj_xml_find_rec( const pj_xml_node *parent, 
				      const pj_str_t *name,
				      const void *data, 
				      pj_bool_t (*match)(const pj_xml_node*, 
							 const void*))
{
    const pj_xml_node *node = (const pj_xml_node *)parent->node_head.next;

    if (!name && !match)
	return NULL;

    while (node != (const pj_xml_node*) &parent->node_head) {
	pj_xml_node *found;

	if (name) {
	    if (pj_stricmp(&node->name, name)==0) {
		if (match) {
		    if (match(node, data))
			return (pj_xml_node*)node;
		} else {
		    return (pj_xml_node*)node;
		}
	    }

	} else if (match) {
	    if (match(node, data))
		return (pj_xml_node*)node;
	}

	found = pj_xml_find_rec(node, name, data, match);
	if (found)
	    return found;

	node = node->next;
    }
    return NULL;
}

Project: pjproject
File: xml.c

static pj_xml_node *xml_parse_node( pj_pool_t *pool, pj_scanner *scanner)
{
    pj_xml_node *node;
    pj_str_t end_name;

    PJ_CHECK_STACK();

    if (*scanner->curptr != '<')
	on_syntax_error(scanner);

    /* Handle Processing Instructino (PI) construct (i.e. "<?") */
    if (*scanner->curptr == '<' && *(scanner->curptr+1) == '?') {
	pj_scan_advance_n(scanner, 2, PJ_FALSE);
	for (;;) {
	    pj_str_t dummy;
	    pj_scan_get_until_ch(scanner, '?', &dummy);
	    if (*scanner->curptr=='?' && *(scanner->curptr+1)=='>') {
		pj_scan_advance_n(scanner, 2, PJ_TRUE);
		break;
	    } else {
		pj_scan_advance_n(scanner, 1, PJ_FALSE);
	    }
	}
	return xml_parse_node(pool, scanner);
    }

    /* Handle comments construct (i.e. "<!") */
    if (pj_scan_strcmp(scanner, "<!", 2) == 0) {
	pj_scan_advance_n(scanner, 2, PJ_FALSE);
	for (;;) {
	    pj_str_t dummy;
	    pj_scan_get_until_ch(scanner, '>', &dummy);
	    if (pj_scan_strcmp(scanner, ">", 1) == 0) {
		pj_scan_advance_n(scanner, 1, PJ_TRUE);
		break;
	    } else {
		pj_scan_advance_n(scanner, 1, PJ_FALSE);
	    }
	}
	return xml_parse_node(pool, scanner);
    }

    /* Alloc node. */
    node = alloc_node(pool);

    /* Get '<' */
    pj_scan_get_char(scanner);

    /* Get node name. */
    pj_scan_get_until_chr( scanner, " />\t\r\n", &node->name);

    /* Get attributes. */
    while (*scanner->curptr != '>' && *scanner->curptr != '/') {
	pj_xml_attr *attr = alloc_attr(pool);
	
	pj_scan_get_until_chr( scanner, "=> \t\r\n", &attr->name);
	if (*scanner->curptr == '=') {
	    pj_scan_get_char( scanner );
            pj_scan_get_quotes(scanner, "\"'", "\"'", 2, &attr->value);
	    /* remove quote characters */
	    ++attr->value.ptr;
	    attr->value.slen -= 2;
	}
	
	pj_list_push_back( &node->attr_head, attr );
    }

    if (*scanner->curptr == '/') {
	pj_scan_get_char(scanner);
	if (pj_scan_get_char(scanner) != '>')
	    on_syntax_error(scanner);
	return node;
    }

    /* Enclosing bracket. */
    if (pj_scan_get_char(scanner) != '>')
	on_syntax_error(scanner);

    /* Sub nodes. */
    while (*scanner->curptr == '<' && *(scanner->curptr+1) != '/'
				   && *(scanner->curptr+1) != '!')
    {
	pj_xml_node *sub_node = xml_parse_node(pool, scanner);
	pj_list_push_back( &node->node_head, sub_node );
    }

    /* Content. */
    if (!pj_scan_is_eof(scanner) && *scanner->curptr != '<') {
	pj_scan_get_until_ch(scanner, '<', &node->content);
    }

    /* CDATA content. */
    if (*scanner->curptr == '<' && *(scanner->curptr+1) == '!' &&
	pj_scan_strcmp(scanner, "<![CDATA[", 9) == 0)
    {
	pj_scan_advance_n(scanner, 9, PJ_FALSE);
	pj_scan_get_until_ch(scanner, ']', &node->content);
	while (pj_scan_strcmp(scanner, "]]>", 3)) {
	    pj_str_t dummy;

	    pj_scan_advance_n(scanner, 1, PJ_FALSE);
	    pj_scan_get_until_ch(scanner, ']', &dummy);
	}
	node->content.slen = scanner->curptr - node->content.ptr;
	pj_scan_advance_n(scanner, 3, PJ_TRUE);
    }

    /* Enclosing node. */
    if (pj_scan_get_char(scanner) != '<' || pj_scan_get_char(scanner) != '/')
	on_syntax_error(scanner);

    pj_scan_get_until_chr(scanner, " \t>", &end_name);

    /* Compare name. */
    if (pj_stricmp(&node->name, &end_name) != 0)
	on_syntax_error(scanner);

    /* Enclosing '>' */
    if (pj_scan_get_char(scanner) != '>')
	on_syntax_error(scanner);

    return node;
}

Project: pjproject
File: xml.c

PJ_DEF(pj_xml_attr*) pj_xml_attr_new( pj_pool_t *pool, const pj_str_t *name,
				      const pj_str_t *value)
{
    pj_xml_attr *attr = alloc_attr(pool);
    pj_strdup( pool, &attr->name, name);
    pj_strdup( pool, &attr->value, value);
    return attr;
}