Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/adapter/web-standard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,35 @@ export const WebStandardAdapter: ElysiaAdapter = {
`if(c.body[key]) continue\n` +
`const value=form.getAll(key)\n` +
`const finalValue=value.length===1?value[0]:value\n` +
`if(key.includes('.')){` +
`if(key.includes('.')||key.includes('[')){` +
`const keys=key.split('.')\n` +
`const lastKey=keys.pop()\n` +
`let current=c.body\n` +
`for(const k of keys){` +
`const arrayMatch=k.match(/^(.+)\\[(\\d+)\\]$/)\n` +
`if(arrayMatch){` +
`const arrayKey=arrayMatch[1]\n` +
`const index=parseInt(arrayMatch[2],10)\n` +
`if(!(arrayKey in current))current[arrayKey]=[]\n` +
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Potential prototype pollution vulnerability: The array key handling doesn't sanitize dangerous property names like __proto__, constructor, or prototype. A malicious form key like __proto__[0] or items.__proto__[0] could manipulate object prototypes. Consider adding a blocklist check before processing keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/adapter/web-standard/index.ts, line 64:

<comment>Potential prototype pollution vulnerability: The array key handling doesn't sanitize dangerous property names like `__proto__`, `constructor`, or `prototype`. A malicious form key like `__proto__[0]` or `items.__proto__[0]` could manipulate object prototypes. Consider adding a blocklist check before processing keys.</comment>

<file context>
@@ -52,16 +52,35 @@ export const WebStandardAdapter: ElysiaAdapter = {
+					`if(arrayMatch){` +
+					`const arrayKey=arrayMatch[1]\n` +
+					`const index=parseInt(arrayMatch[2],10)\n` +
+					`if(!(arrayKey in current))current[arrayKey]=[]\n` +
+					`if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` +
+					`if(!current[arrayKey][index])current[arrayKey][index]={}\n` +
</file context>
Fix with Cubic

`if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` +
`if(!current[arrayKey][index])current[arrayKey][index]={}\n` +
`current=current[arrayKey][index]` +
`}else{` +
`if(!(k in current)||typeof current[k]!=='object'||current[k]===null)` +
`current[k]={}\n` +
`current=current[k]` +
`}` +
`}\n` +
`const lastArrayMatch=lastKey.match(/^(.+)\\[(\\d+)\\]$/)\n` +
`if(lastArrayMatch){` +
`const arrayKey=lastArrayMatch[1]\n` +
`const index=parseInt(lastArrayMatch[2],10)\n` +
`if(!(arrayKey in current))current[arrayKey]=[]\n` +
`if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` +
`current[arrayKey][index]=finalValue` +
`}else{` +
`current[lastKey]=finalValue` +
`}` +
Comment on lines +55 to +83
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden array-index traversal against primitives.

If a prior field sets arrayKey[index] to a primitive (e.g., items[0]=foo) and a later field uses items[0].id, current becomes a primitive and the next assignment throws. Consider ensuring the element is an object before descending.

🐛 Proposed fix
- `if(!current[arrayKey][index])current[arrayKey][index]={}\n` +
+ `if(!current[arrayKey][index]||typeof current[arrayKey][index]!=='object'||current[arrayKey][index]===null)current[arrayKey][index]={}\n` +
🤖 Prompt for AI Agents
In `@src/adapter/web-standard/index.ts` around lines 55 - 83, The traversal that
builds nested objects/arrays (using variables keys, lastKey, current,
arrayMatch/lastArrayMatch, arrayKey, index, finalValue) can break when an
existing array element is a primitive; before descending into
current[arrayKey][index] or assigning current[k] as the new current, ensure the
target is an object (typeof === 'object' && current !== null) and if it is
missing or not an object replace it with {} (e.g., when handling arrayMatch and
lastArrayMatch check if current[arrayKey][index] exists and is an object,
otherwise set current[arrayKey][index]={}; similarly when handling non-array
keys ensure current[k] is an object before assigning current=current[k]). This
prevents primitive values from being treated as containers during later
dot/array-index traversals.

`}else c.body[key]=finalValue` +
`}`
)
Expand Down
47 changes: 40 additions & 7 deletions src/dynamic-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,50 @@ const setNestedValue = (
path: string,
value: any
) => {
// Split by dots, but preserve array indices
const keys = path.split('.')
const lastKey = keys.pop()!

let current = obj
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
// Check if key has array index notation: key[0], key[1], etc.
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)

if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing bounds check on array index from user input. Consider adding a maximum index limit (e.g., if (index > 10000) return or throw an error) to prevent potential DoS via sparse arrays with extremely large indices like items[999999999999].

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/dynamic-handle.ts, line 43:

<comment>Missing bounds check on array index from user input. Consider adding a maximum index limit (e.g., `if (index > 10000) return` or throw an error) to prevent potential DoS via sparse arrays with extremely large indices like `items[999999999999]`.</comment>

<file context>
@@ -29,17 +29,50 @@ const setNestedValue = (
+
+		if (arrayMatch) {
+			const [, arrayKey, indexStr] = arrayMatch
+			const index = parseInt(indexStr, 10)
+
+			// Initialize array if needed
</file context>
Fix with Cubic


// Initialize array if needed
if (!(arrayKey in current)) current[arrayKey] = []

// Ensure it's an array
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []

// Initialize object at index if needed
if (!current[arrayKey][index]) current[arrayKey][index] = {}

current = current[arrayKey][index]
} else {
// Regular object property
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
}
}

current[lastKey] = value
// Handle array index in last key
const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)

if (!(arrayKey in current)) current[arrayKey] = []
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []

current[arrayKey][index] = value
} else {
current[lastKey] = value
}
Comment on lines +32 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent crashes when array elements are non-objects.

If an array index already contains a primitive and a later path descends further (e.g., items[0]=1 then items[0].id), this will throw at assignment. Guard by coercing non-objects to {} before descending.

🐛 Proposed fix
-			if (!current[arrayKey][index]) current[arrayKey][index] = {}
+			if (
+				!current[arrayKey][index] ||
+				typeof current[arrayKey][index] !== 'object' ||
+				current[arrayKey][index] === null
+			)
+				current[arrayKey][index] = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Split by dots, but preserve array indices
const keys = path.split('.')
const lastKey = keys.pop()!
let current = obj
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
// Check if key has array index notation: key[0], key[1], etc.
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
// Initialize array if needed
if (!(arrayKey in current)) current[arrayKey] = []
// Ensure it's an array
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
// Initialize object at index if needed
if (!current[arrayKey][index]) current[arrayKey][index] = {}
current = current[arrayKey][index]
} else {
// Regular object property
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
}
}
current[lastKey] = value
// Handle array index in last key
const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
if (!(arrayKey in current)) current[arrayKey] = []
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
current[arrayKey][index] = value
} else {
current[lastKey] = value
}
// Split by dots, but preserve array indices
const keys = path.split('.')
const lastKey = keys.pop()!
let current = obj
for (const key of keys) {
// Check if key has array index notation: key[0], key[1], etc.
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
// Initialize array if needed
if (!(arrayKey in current)) current[arrayKey] = []
// Ensure it's an array
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
// Initialize object at index if needed
if (
!current[arrayKey][index] ||
typeof current[arrayKey][index] !== 'object' ||
current[arrayKey][index] === null
)
current[arrayKey][index] = {}
current = current[arrayKey][index]
} else {
// Regular object property
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null)
current[key] = {}
current = current[key]
}
}
// Handle array index in last key
const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, indexStr] = arrayMatch
const index = parseInt(indexStr, 10)
if (!(arrayKey in current)) current[arrayKey] = []
if (!Array.isArray(current[arrayKey])) current[arrayKey] = []
current[arrayKey][index] = value
} else {
current[lastKey] = value
}
🤖 Prompt for AI Agents
In `@src/dynamic-handle.ts` around lines 32 - 75, The code crashes when an array
slot contains a primitive and later code tries to descend into it (e.g.,
items[0] = 1 then items[0].id); in the keys loop and before descending into an
array element you must coerce non-objects to {}. Update the array branch where
you do if (!current[arrayKey][index]) current[arrayKey][index] = {} to instead
check whether typeof current[arrayKey][index] !== 'object' ||
current[arrayKey][index] === null (and set it to {} in that case), and apply the
same defensive coerce-before-descend logic for any place that assumes
current[arrayKey][index] is an object (the array handling in the main keys loop
and before descending into lastKey when arrayMatch is present).

}

const injectDefaultValues = (
Expand Down Expand Up @@ -159,7 +192,7 @@ export const createDynamicHandler = (app: AnyElysia) => {
const value = form.getAll(key)
const finalValue = value.length === 1 ? value[0] : value

if (key.includes('.'))
if (key.includes('.') || key.includes('['))
setNestedValue(body, key, finalValue)
else body[key] = finalValue
}
Expand Down Expand Up @@ -220,7 +253,7 @@ export const createDynamicHandler = (app: AnyElysia) => {
const value = form.getAll(key)
const finalValue = value.length === 1 ? value[0] : value

if (key.includes('.'))
if (key.includes('.') || key.includes('['))
setNestedValue(body, key, finalValue)
else body[key] = finalValue
}
Expand Down Expand Up @@ -290,7 +323,7 @@ export const createDynamicHandler = (app: AnyElysia) => {
const value = form.getAll(key)
const finalValue = value.length === 1 ? value[0] : value

if (key.includes('.'))
if (key.includes('.') || key.includes('['))
setNestedValue(body, key, finalValue)
else body[key] = finalValue
}
Expand Down
66 changes: 66 additions & 0 deletions test/validator/body.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,4 +1298,70 @@ describe('Body Validator', () => {
expect(result.nestedName).toBe('Jane')
expect(result.nestedFileSize).toBeGreaterThan(0)
})

it('handle complex nested array with files', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
productName: body.name,
createFilesCount: body.images.create.length,
updateCount: body.images.update.length,
images: {
create: body.images.create.map((f) => f.size),
update: body.images.update.map((f) => ({
id: f.id,
altText: f.altText,
imgSize: f.img.size
}))
}
}),
{
body: t.Object({
name: t.String(),
images: t.Object({
create: t.Files(),
update: t.Array(
t.Object({
id: t.String(),
img: t.File(),
altText: t.String()
})
)
})
})
}
)

const formData = new FormData()
formData.append('name', 'My Product')
formData.append('images.create[0]', Bun.file('test/images/millenium.jpg'))
formData.append('images.create[1]', Bun.file('test/images/kozeki-ui.webp'))
formData.append('images.update[0].id', '1')
formData.append('images.update[0].img', Bun.file('test/images/midori.png'))
formData.append('images.update[0].altText', 'First image')
formData.append('images.update[1].id', '2')
formData.append('images.update[1].img', Bun.file('test/images/aris-yuzu.jpg'))
formData.append('images.update[1].altText', 'Second image')

const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)

const result = await response.json()
expect(response.status).toBe(200)
expect(result.productName).toBe('My Product')
expect(result.createFilesCount).toBe(2)
expect(result.updateCount).toBe(2)
expect(result.images.create[0]).toBeGreaterThan(0)
expect(result.images.create[1]).toBeGreaterThan(0)
expect(result.images.update[0].id).toBe('1')
expect(result.images.update[0].altText).toBe('First image')
expect(result.images.update[0].imgSize).toBeGreaterThan(0)
expect(result.images.update[1].id).toBe('2')
expect(result.images.update[1].altText).toBe('Second image')
expect(result.images.update[1].imgSize).toBeGreaterThan(0)
})
})