Skip to content

Commit

Permalink
XWIKI-21424: Use CSRF token in the realtime HTML Converter API
Browse files Browse the repository at this point in the history
* Add a mandatory CSRF token to use RTFrontend.ConvertHTML
* Add a page test for RTFrontend.ConvertHTML that checks for the proper use of the CSRF token
* Improve URL creation and add Translations.xml

(cherry picked from commit 4896712)
  • Loading branch information
pjeanjean authored and michitux committed Oct 24, 2023
1 parent 8a92cb4 commit d88da45
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 4 deletions.
Expand Up @@ -75,5 +75,27 @@
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Test dependencies. -->
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-test-page</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<!-- Provides the component list for RenderingScriptService. -->
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-rendering-xwiki</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-rendering-configuration-default</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Expand Up @@ -20,7 +20,7 @@
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->

<xwikidoc version="1.4" reference="RTFrontend.ConvertHTML" locale="">
<xwikidoc version="1.5" reference="RTFrontend.ConvertHTML" locale="">
<web>RTFrontend</web>
<name>ConvertHTML</name>
<language/>
Expand Down Expand Up @@ -52,8 +52,13 @@
'documentReference': $documentReference
})
#if ($xcontext.action == 'get')
## FIXME: We shouldn't depend on CKEditor. This code should work independent of the configured WYSIWYG editor.
$xwiki.getDocument('CKEditor.ContentSheet').getRenderedContent()
## Check that the CSRF token matches the user.
#if (!$services.csrf.isTokenValid($request.form_token))
$response.sendError(403, $services.localization.render('rtfFrontend.convertHtml.invalidCsrfToken'))
#else
## FIXME: We shouldn't depend on CKEditor. This code should work independent of the configured WYSIWYG editor.
$xwiki.getDocument('CKEditor.ContentSheet').getRenderedContent()
#end
#end
{{/velocity}}</content>
</xwikidoc>
@@ -0,0 +1,77 @@
<?xml version="1.1" encoding="UTF-8"?>

<!--
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->

<xwikidoc version="1.5" reference="XWiki.Realtime.Translations" locale="">
<web>XWiki.Realtime</web>
<name>Translations</name>
<language/>
<defaultLanguage>en</defaultLanguage>
<translation>0</translation>
<creator>xwiki:XWiki.Admin</creator>
<parent>Main.WebHome</parent>
<author>xwiki:XWiki.Admin</author>
<contentAuthor>xwiki:XWiki.Admin</contentAuthor>
<version>1.1</version>
<title/>
<comment/>
<minorEdit>false</minorEdit>
<syntaxId>plain/1.0</syntaxId>
<hidden>true</hidden>
<content>rtfFrontend.convertHtml.invalidCsrfToken=Invalid CSRF Token</content>
<object>
<name>XWiki.Realtime.Translations</name>
<number>0</number>
<className>XWiki.TranslationDocumentClass</className>
<guid>41105c55-62ec-47bc-9dd0-c502cad68de6</guid>
<class>
<name>XWiki.TranslationDocumentClass</name>
<customClass/>
<customMapping/>
<defaultViewSheet/>
<defaultEditSheet/>
<defaultWeb/>
<nameField/>
<validationScript/>
<scope>
<cache>0</cache>
<disabled>0</disabled>
<displayType>select</displayType>
<freeText>forbidden</freeText>
<largeStorage>0</largeStorage>
<multiSelect>0</multiSelect>
<name>scope</name>
<number>1</number>
<prettyName>Scope</prettyName>
<relationalStorage>0</relationalStorage>
<separator> </separator>
<separators>|, </separators>
<size>1</size>
<unmodifiable>0</unmodifiable>
<values>GLOBAL|WIKI|USER|ON_DEMAND</values>
<classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
</scope>
</class>
<property>
<scope>WIKI</scope>
</property>
</object>
</xwikidoc>
@@ -0,0 +1,118 @@
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* 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.realtime.ui;

import javax.inject.Provider;

import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.csrf.script.CSRFTokenScriptService;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.RenderingScriptServiceComponentList;
import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
import org.xwiki.script.service.ScriptService;
import org.xwiki.test.page.HTML50ComponentList;
import org.xwiki.test.page.PageTest;
import org.xwiki.test.page.XWikiSyntax21ComponentList;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.user.api.XWikiRightService;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RenderingScriptServiceComponentList
@DefaultRenderingConfigurationComponentList
@HTML50ComponentList
@XWikiSyntax21ComponentList
class ConvertHTMLPageTest extends PageTest
{

private static final String WIKI_NAME = "xwiki";

private static final String XWIKI_SPACE = "RTFrontend";

private static final DocumentReference RTF_FRONTEND_CONVERT_HTML =
new DocumentReference(WIKI_NAME, XWIKI_SPACE, "ConvertHTML");

private static final String CSRF_TOKEN = "a0a0a0a0";

private CSRFTokenScriptService tokenService;

@BeforeEach
void setUp() throws Exception
{
// Mock the Token Service to get a consistent CSRF token throughout the tests.
this.tokenService = this.oldcore.getMocker().registerMockComponent(ScriptService.class, "csrf",
CSRFTokenScriptService.class, true);
when(this.tokenService.isTokenValid(CSRF_TOKEN)).thenReturn(true);

this.xwiki.initializeMandatoryDocuments(this.context);

this.context = mock(XWikiContext.class);
Provider<XWikiContext> xcontextProvider =
this.componentManager.registerMockComponent(XWikiContext.TYPE_PROVIDER);
when(xcontextProvider.get()).thenReturn(this.context);
when(this.context.getRequest()).thenReturn(this.request);
when(this.context.getResponse()).thenReturn(this.response);
when(this.context.getWiki()).thenReturn(this.xwiki);

// Fake programming access level to display the complete page.
XWikiRightService rightService = this.oldcore.getMockRightService();
when(this.xwiki.getRightService()).thenReturn(rightService);
when(rightService.hasProgrammingRights(this.context)).thenReturn(true);
this.response = spy(this.response);
when(this.context.getResponse()).thenReturn(this.response);
}

@Test
void checkValidCSRFToken() throws Exception
{
when(this.context.getAction()).thenReturn("get");
this.request.put("text", "Hello");
this.request.put("form_token", CSRF_TOKEN);
Document result = renderHTMLPage(RTF_FRONTEND_CONVERT_HTML);

verify(this.response, never()).setStatus(anyInt());
verify(this.tokenService).isTokenValid(CSRF_TOKEN);
assertEquals("$xwiki.getDocument('CKEditor.ContentSheet').getRenderedContent()",
result.getElementsByTag("body").text());
}

@Test
void checkInvalidCSRFToken() throws Exception
{
String wrongToken = "wrong_token";
when(this.context.getAction()).thenReturn("get");
this.request.put("text", "Hello");
this.request.put("form_token", wrongToken);
Document result = renderHTMLPage(RTF_FRONTEND_CONVERT_HTML);

verify(this.response).sendError(403, "rtfFrontend.convertHtml.invalidCsrfToken");
verify(this.tokenService).isTokenValid(wrongToken);
assertEquals("", result.getElementsByTag("body").text());
}
}
Expand Up @@ -71,7 +71,11 @@ define('xwiki-realtime-loader', [
var userReference = xm.userReference ? XWiki.Model.serialize(xm.userReference) : 'xwiki:XWiki.XWikiGuest';
return {
WebsocketURL: realtimeConfig.webSocketURL,
htmlConverterUrl: new XWiki.Document('ConvertHTML', 'RTFrontend').getURL('get', 'xpage=plain&outputSyntax=plain'),
htmlConverterUrl: new XWiki.Document('ConvertHTML', 'RTFrontend').getURL('get', $.param({
'xpage': 'plain',
'outputSyntax': 'plain',
'form_token': document.documentElement.dataset.xwikiFormToken
})),
// userId === <userReference>-encoded(<userName>)%2d<randomNumber>
userName: userReference + '-' + encodeURIComponent(realtimeConfig.user.name + '-').replace(/-/g, '%2d') +
String(Math.random()).substring(2),
Expand Down

0 comments on commit d88da45

Please sign in to comment.