unraid / unraid/api

VIEWER API key with only READ_ANY can execute notification mutations

Open
#2,065 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
113
Forks
22
Avg merge
10h 40m
Merged PRs (30d)
13

Description

Environment

Unraid OS Version: 7.2.4
Unraid API: 4.29.2+c39b0b26
Authentication: API key, role VIEWER, granular permissions, READ_ANY only
Transport: local GraphQL endpoint

Are you using a reverse proxy? No — tested against the server directly.

No hostnames, addresses, API-key values, or registration data are included.

Pre-submission Checklist

  • I have verified that my Unraid OS is up to date
  • I have tested this issue by accessing my server directly (not through a reverse proxy)
  • This is not an Unraid Connect related issue

Issue Description

An API key holding role VIEWER and only READ_ANY permissions can successfully
execute notification mutations. Every other resource correctly rejects mutations
for the same key, so this looks like a missing authorization guard on the notification
mutation resolvers rather than a problem with the permission model itself.

Key scope, as reported by the API when queried with the key under test:

query { me { roles permissions { resource actions } } }
roles: ["VIEWER"]
permissions: 17 resources, every one ["READ_ANY"]
  ARRAY, DISK, DOCKER, LOGS, VARS, NETWORK, INFO, NOTIFICATIONS,
  SERVICES, SHARE, VMS, OS, ONLINE, REGISTRATION, FLASH, DISPLAY, CONFIG

No CREATE_*, UPDATE_*, or DELETE_* action is granted on any resource. The
NOTIFICATIONS entry is exactly {"resource": "NOTIFICATIONS", "actions": ["READ_ANY"]}.

Steps to Reproduce

  1. Create an API key with role VIEWER and granular permissions including
    NOTIFICATIONS:READ_ANY. Grant no write action on any resource.

  2. Confirm the key's scope contains only READ_ANY entries:

    query { me { roles permissions { resource actions } } }
    
  3. Issue a notification mutation with that key:

    mutation {
      createNotification(input: {
        title: "scope-test"
        subject: "scope-test"
        description: "permission probe"
        importance: INFO
      }) { id }
    }
    

    HTTP 200:

    { "data": { "createNotification": { "id": "..." } } }
    

    The notification is created and appears in the WebGUI.

  4. Contrast with a correctly-guarded mutation using the same key:

    mutation { array { setState(input: { desiredState: STOP }) { state } } }
    
    { "errors": [ { "message": "Forbidden resource",
                    "extensions": { "code": "FORBIDDEN",
                                    "originalError": { "statusCode": 403 } } } ],
      "data": null }
    

Expected Behavior

All mutations are rejected with Forbidden resource, consistent with a key that holds
only READ_ANY.

Actual Behavior

Notification mutations succeed. Mutations on every other resource tested are correctly
refused.

Mutation Expected Actual
createNotification Forbidden Succeeds
archiveAll Forbidden Succeeds
deleteArchivedNotifications Forbidden Succeeds
array { setState } Forbidden Forbidden (correct)
docker { start } Forbidden Forbidden (correct)
vm { start } Forbidden Forbidden (correct)
parityCheck { start } Forbidden Forbidden (correct)
apiKey { create } Forbidden Forbidden (correct)
updateSettings Forbidden Forbidden (correct)

Results were reproduced across two separate runs.

Impact

This breaks the security guarantee of the VIEWER role. A key documented and intended
as read-only can write to the notification subsystem.

The deletion path is the more meaningful half. deleteArchivedNotifications and
archiveAll let a read-only key destroy or hide notification history, including
warnings a user has not yet seen (SMART warnings, parity errors). That reads as an
alert-suppression / audit-integrity issue rather than a cosmetic one.

This is becoming more relevant as third-party integrations standardise on VIEWER
keys for monitoring — Home Assistant bridges, MCP servers, dashboards. Users granting
a VIEWER key to an external tool reasonably expect that tool cannot mutate anything.

Suggested Fix

Apply the authorization guard already used by the array / docker / vm / settings
mutation resolvers to the notification mutation resolvers, requiring an appropriate
write action (NOTIFICATIONS:CREATE_ANY / UPDATE_ANY / DELETE_ANY) rather than
being satisfied by READ_ANY.

Additional Context

Found while configuring a read-only integration against the GraphQL API. Any test
notification created during verification was archived and deleted afterwards; no other
server state was modified.

Possibly related, same authorization subsystem: #2060.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by locating the notification mutation resolvers and compare their authorization handling with the array, Docker, VM, and settings mutation resolvers mentioned in the report; review related authorization work in issue #2060. Reproduce the listed GraphQL mutations with a VIEWER key, then verify that createNotification, archiveAll, and deleteArchivedNotifications reject READ_ANY-only access while permitted reads still work.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, typescript
Domain
api, authorization, backend, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.