CVE-2023-26479: Improper Handling of Exceptional Conditions (CWE-755)
value: XWiki Platform is a generic wiki platform. Starting in version 6.0, users with write rights can insert well-formed content that is not handled well by the parser. As a consequence, some pages becomes unusable, including the user index (if the page containing the faulty content is a user page) and the page index.

Note that on the page, the normal UI is completely missing and it is not possible to open the editor directly to revert the change as the stack overflow is already triggered while getting the title of the document. This means that it is quite difficult to remove this content once inserted.

This has been patched in XWiki 13.10.10, 14.4.6, and 14.9-rc-1. A temporary workaround to avoid Stack Overflow errors is to increase the memory allocated to the stack by using the `-Xss` JVM parameter (e.g., `-Xss32m`). This should allow the parser to pass and to fix the faulty content. The consequences for other aspects of the system (e.g., performance) are unknown, and this workaround should be only be used as a temporary solution. The workaround does not prevent the issue occurring again with other content. Consequently, it is strongly advised to upgrade to a version where the issue has been patched.
CWE description: The product does not handle or incorrectly handles an exceptional condition.
CVSS v3 base score: 6.5
Published: 2023-03-02T18:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/main/java/org/xwiki/rendering/internal/parser/DefaultContentParser.java, xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/test/java/org/xwiki/rendering/internal/parser/DefaultContentParserTest.java
Commit date: 2022-10-13 09:24:10 +0200
Commit message: XWIKI-19838: Improve handling of unexpected parser errors

Diff: diff --git a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/main/java/org/xwiki/rendering/internal/parser/DefaultContentParser.java b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/main/java/org/xwiki/rendering/internal/parser/DefaultContentParser.java
index 36d116698ee5b..a08bb0635b7a2 100644
--- a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/main/java/org/xwiki/rendering/internal/parser/DefaultContentParser.java
+++ b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/main/java/org/xwiki/rendering/internal/parser/DefaultContentParser.java
@@ -57,11 +57,19 @@ public class DefaultContentParser implements ContentParser
     private EntityReferenceSerializer<String> serializer;
 
     @Override
     public XDOM parse(String content, Syntax syntax) throws ParseException, MissingParserException
     {
-        return getParser(syntax).parse(new StringReader(content == null ? "" : content));
+        Parser parser = getParser(syntax);
+        try {
+            return parser.parse(new StringReader(content == null ? "" : content));
+        } catch (StackOverflowError | Exception e) {
+            // All exceptions as well as stack overflow errors are captured and wrapped in parse exceptions to make sure
+            // that they are handled correctly by the callers. Without this, some parsing issues can be badly handled,
+            // leading to instability issues.
+            throw new ParseException(String.format("Failed to parse with syntax [%s].", syntax.toIdString()), e);
+        }
     }
 
     @Override
     public XDOM parse(String content, Syntax syntax, EntityReference source) throws ParseException,
         MissingParserException
diff --git a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/test/java/org/xwiki/rendering/internal/parser/DefaultContentParserTest.java b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/test/java/org/xwiki/rendering/internal/parser/DefaultContentParserTest.java
index 4cc4a5dcbce60..2ab897234f3f5 100644
--- a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/test/java/org/xwiki/rendering/internal/parser/DefaultContentParserTest.java
+++ b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-parser/src/test/java/org/xwiki/rendering/internal/parser/DefaultContentParserTest.java
@@ -17,95 +17,124 @@
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.rendering.internal.parser;
 
-import static org.hamcrest.CoreMatchers.any;
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.mockito.hamcrest.MockitoHamcrest.argThat;
-import static org.mockito.Mockito.when;
-
 import java.io.Reader;
-import java.util.Collections;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.xwiki.component.internal.ContextComponentManagerProvider;
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReferenceSerializer;
-import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.XDOM;
 import org.xwiki.rendering.listener.MetaData;
-import org.xwiki.rendering.parser.ContentParser;
 import org.xwiki.rendering.parser.MissingParserException;
+import org.xwiki.rendering.parser.ParseException;
 import org.xwiki.rendering.parser.Parser;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.test.annotation.ComponentList;
-import org.xwiki.test.mockito.MockitoComponentMockingRule;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.test.mockito.MockitoComponentManager;
+
+import static java.util.Collections.emptyList;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.mockito.hamcrest.MockitoHamcrest.argThat;
+import static org.xwiki.rendering.syntax.Syntax.PLAIN_1_0;
+import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_1;
 
 /**
- * Unit tests for {@link org.xwiki.rendering.internal.parser.DefaultContentParser}.
+ * Unit tests for {@link DefaultContentParser}.
  *
  * @version $Id$
  * @since 6.0M2
  */
+@ComponentTest
 @ComponentList(ContextComponentManagerProvider.class)
-public class DefaultContentParserTest
+class DefaultContentParserTest
 {
-    @Rule
-    public final MockitoComponentMockingRule<ContentParser> mocker =
-        new MockitoComponentMockingRule<>(DefaultContentParser.class);
-
-    @Rule
-    public ExpectedException thrown = ExpectedException.none();
-
     private static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("wiki", "space", "page");
+
     private static final String SOURCE = "wiki:space.page";
 
-    @Before
-    public void configure() throws Exception
-    {
-        Parser parser = mocker.registerMockComponent(Parser.class, Syntax.PLAIN_1_0.toIdString());
-        when(parser.parse(argThat(any(Reader.class)))).thenReturn(new XDOM(Collections.<Block>emptyList()));
+    @InjectMockComponents
+    private DefaultContentParser defaultContentParser;
+
+    @MockComponent
+    private EntityReferenceSerializer<String> serializer;
 
-        EntityReferenceSerializer<String> serializer = mocker.getInstance(EntityReferenceSerializer.TYPE_STRING);
-        when(serializer.serialize(DOCUMENT_REFERENCE)).thenReturn(SOURCE);
+    private Parser plain10parser;
+
+    @BeforeEach
+    void setUp(MockitoComponentManager componentManager) throws Exception
+    {
+        this.plain10parser = componentManager.registerMockComponent(Parser.class, PLAIN_1_0.toIdString());
+        when(this.plain10parser.parse(argThat(CoreMatchers.any(Reader.class)))).thenReturn(new XDOM(emptyList()));
+        when(this.serializer.serialize(DOCUMENT_REFERENCE)).thenReturn(SOURCE);
     }
 
     @Test
-    public void parseHasNoMetadataSource() throws Exception
+    void parseHasNoMetadataSource() throws Exception
     {
-        XDOM xdom = mocker.getComponentUnderTest().parse("", Syntax.PLAIN_1_0);
+        XDOM xdom = this.defaultContentParser.parse("", PLAIN_1_0);
 
         assertThat(xdom.getMetaData().getMetaData(MetaData.SOURCE), nullValue());
     }
 
     @Test
-    public void parseIsAddingMetadataSource() throws Exception
+    void parseIsAddingMetadataSource() throws Exception
     {
-        XDOM xdom = mocker.getComponentUnderTest().parse("", Syntax.PLAIN_1_0, DOCUMENT_REFERENCE);
+        XDOM xdom = this.defaultContentParser.parse("", PLAIN_1_0, DOCUMENT_REFERENCE);
 
         assertThat(xdom.getMetaData().getMetaData(MetaData.SOURCE), equalTo(SOURCE));
     }
 
     @Test
-    public void parseWhenNoParser() throws Exception
+    void parseWhenNoParser()
+    {
+        MissingParserException missingParserException = assertThrows(MissingParserException.class,
+            () -> this.defaultContentParser.parse("", XWIKI_2_1, DOCUMENT_REFERENCE));
+        assertEquals(ComponentLookupException.class, missingParserException.getCause().getClass());
+        assertEquals("Failed to find a parser for syntax [XWiki 2.1]", missingParserException.getMessage());
+    }
+
+    @Test
+    void parseWhenNoParserFail() throws Exception
+    {
+        when(this.plain10parser.parse(any())).thenThrow(StackOverflowError.class);
+
+        ParseException parseErrorException =
+            assertThrows(ParseException.class, () -> this.defaultContentParser.parse("content", PLAIN_1_0));
+
+        assertEquals(StackOverflowError.class, parseErrorException.getCause().getClass());
+        assertEquals("Failed to parse with syntax [plain/1.0].", parseErrorException.getMessage());
+    }
+
+    @Test
+    void parseWhenNoParserFailWithSource() throws Exception
     {
-        thrown.expect(MissingParserException.class);
-        thrown.expectMessage("Failed to find a parser for syntax [XWiki 2.1]");
-        thrown.expectCause(any(ComponentLookupException.class));
-        mocker.getComponentUnderTest().parse("", Syntax.XWIKI_2_1, DOCUMENT_REFERENCE);
+        when(this.plain10parser.parse(any())).thenThrow(StackOverflowError.class);
+
+        ParseException parseErrorException =
+            assertThrows(ParseException.class, () -> this.defaultContentParser.parse("content", PLAIN_1_0, null));
+
+        assertEquals(StackOverflowError.class, parseErrorException.getCause().getClass());
+        assertEquals("Failed to parse with syntax [plain/1.0].", parseErrorException.getMessage());
     }
 
     @Test
-    public void parseWhenNullSource() throws Exception
+    void parseWhenNullSource() throws Exception
     {
-        XDOM xdom = mocker.getComponentUnderTest().parse(null, Syntax.PLAIN_1_0);
+        XDOM xdom = this.defaultContentParser.parse(null, Syntax.PLAIN_1_0);
         assertEquals(0, xdom.getChildren().size());
     }
 }

Vulnerable code:         return getParser(syntax).parse(new StringReader(content == null ? "" : content));
import static org.hamcrest.CoreMatchers.any;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
import static org.mockito.Mockito.when;

import java.util.Collections;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.parser.ContentParser;
import org.xwiki.test.mockito.MockitoComponentMockingRule;
 * Unit tests for {@link org.xwiki.rendering.internal.parser.DefaultContentParser}.
public class DefaultContentParserTest
    @Rule
    public final MockitoComponentMockingRule<ContentParser> mocker =
        new MockitoComponentMockingRule<>(DefaultContentParser.class);

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void configure() throws Exception
    {
        Parser parser = mocker.registerMockComponent(Parser.class, Syntax.PLAIN_1_0.toIdString());
        when(parser.parse(argThat(any(Reader.class)))).thenReturn(new XDOM(Collections.<Block>emptyList()));
        EntityReferenceSerializer<String> serializer = mocker.getInstance(EntityReferenceSerializer.TYPE_STRING);
        when(serializer.serialize(DOCUMENT_REFERENCE)).thenReturn(SOURCE);
    public void parseHasNoMetadataSource() throws Exception
        XDOM xdom = mocker.getComponentUnderTest().parse("", Syntax.PLAIN_1_0);
    public void parseIsAddingMetadataSource() throws Exception
        XDOM xdom = mocker.getComponentUnderTest().parse("", Syntax.PLAIN_1_0, DOCUMENT_REFERENCE);
    public void parseWhenNoParser() throws Exception
        thrown.expect(MissingParserException.class);
        thrown.expectMessage("Failed to find a parser for syntax [XWiki 2.1]");
        thrown.expectCause(any(ComponentLookupException.class));
        mocker.getComponentUnderTest().parse("", Syntax.XWIKI_2_1, DOCUMENT_REFERENCE);
    public void parseWhenNullSource() throws Exception
        XDOM xdom = mocker.getComponentUnderTest().parse(null, Syntax.PLAIN_1_0);
Fixed code:         Parser parser = getParser(syntax);
        try {
            return parser.parse(new StringReader(content == null ? "" : content));
        } catch (StackOverflowError | Exception e) {
            // All exceptions as well as stack overflow errors are captured and wrapped in parse exceptions to make sure
            // that they are handled correctly by the callers. Without this, some parsing issues can be badly handled,
            // leading to instability issues.
            throw new ParseException(String.format("Failed to parse with syntax [%s].", syntax.toIdString()), e);
        }
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.rendering.parser.ParseException;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.test.mockito.MockitoComponentManager;

import static java.util.Collections.emptyList;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
import static org.xwiki.rendering.syntax.Syntax.PLAIN_1_0;
import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_1;
 * Unit tests for {@link DefaultContentParser}.
@ComponentTest
class DefaultContentParserTest

    @InjectMockComponents
    private DefaultContentParser defaultContentParser;

    @MockComponent
    private EntityReferenceSerializer<String> serializer;
    private Parser plain10parser;

    @BeforeEach
    void setUp(MockitoComponentManager componentManager) throws Exception
    {
        this.plain10parser = componentManager.registerMockComponent(Parser.class, PLAIN_1_0.toIdString());
        when(this.plain10parser.parse(argThat(CoreMatchers.any(Reader.class)))).thenReturn(new XDOM(emptyList()));
        when(this.serializer.serialize(DOCUMENT_REFERENCE)).thenReturn(SOURCE);
    void parseHasNoMetadataSource() throws Exception
        XDOM xdom = this.defaultContentParser.parse("", PLAIN_1_0);
    void parseIsAddingMetadataSource() throws Exception
        XDOM xdom = this.defaultContentParser.parse("", PLAIN_1_0, DOCUMENT_REFERENCE);
    void parseWhenNoParser()
    {
        MissingParserException missingParserException = assertThrows(MissingParserException.class,
            () -> this.defaultContentParser.parse("", XWIKI_2_1, DOCUMENT_REFERENCE));
        assertEquals(ComponentLookupException.class, missingParserException.getCause().getClass());
        assertEquals("Failed to find a parser for syntax [XWiki 2.1]", missingParserException.getMessage());
    }

    @Test
    void parseWhenNoParserFail() throws Exception
    {
        when(this.plain10parser.parse(any())).thenThrow(StackOverflowError.class);

        ParseException parseErrorException =
            assertThrows(ParseException.class, () -> this.defaultContentParser.parse("content", PLAIN_1_0));

        assertEquals(StackOverflowError.class, parseErrorException.getCause().getClass());
        assertEquals("Failed to parse with syntax [plain/1.0].", parseErrorException.getMessage());
    }

    @Test
    void parseWhenNoParserFailWithSource() throws Exception
        when(this.plain10parser.parse(any())).thenThrow(StackOverflowError.class);

        ParseException parseErrorException =
            assertThrows(ParseException.class, () -> this.defaultContentParser.parse("content", PLAIN_1_0, null));

        assertEquals(StackOverflowError.class, parseErrorException.getCause().getClass());
        assertEquals("Failed to parse with syntax [plain/1.0].", parseErrorException.getMessage());
    void parseWhenNullSource() throws Exception
        XDOM xdom = this.defaultContentParser.parse(null, Syntax.PLAIN_1_0);