Skip to content

Commit ca76ff1

Browse files
matthewpastro-security[bot]
andauthored
Harden server island POST endpoint to use own-property checks (#15765)
Co-authored-by: astro-security[bot] <astro-security[bot]@users.noreply.github.com>
1 parent 0c81216 commit ca76ff1

File tree

3 files changed

+45
-2
lines changed

3 files changed

+45
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Hardens server island POST endpoint validation to use own-property checks for improved consistency

packages/astro/src/core/server-islands/endpoint.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ export async function getRequestData(
8484
const data = JSON.parse(raw);
8585

8686
// Validate that slots is not plaintext
87-
if ('slots' in data && typeof data.slots === 'object') {
87+
if (Object.hasOwn(data, 'slots') && typeof data.slots === 'object') {
8888
return badRequest('Plaintext slots are not allowed. Slots must be encrypted.');
8989
}
9090

9191
// Validate that componentExport is not plaintext
92-
if ('componentExport' in data && typeof data.componentExport === 'string') {
92+
if (Object.hasOwn(data, 'componentExport') && typeof data.componentExport === 'string') {
9393
return badRequest(
9494
'Plaintext componentExport is not allowed. componentExport must be encrypted.',
9595
);

packages/astro/test/units/server-islands/endpoint.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,44 @@ describe('getRequestData', () => {
146146
assert.equal(result.encryptedProps, '');
147147
assert.equal(result.encryptedSlots, '');
148148
});
149+
150+
it('only checks own properties for `slots` validation', async () => {
151+
// Temporarily pollute Object.prototype to simulate inherited properties
152+
Object.prototype.slots = { default: 'polluted' };
153+
try {
154+
const req = makePostRequest({
155+
encryptedComponentExport: 'encExport',
156+
encryptedProps: 'encProps',
157+
encryptedSlots: 'encSlots',
158+
});
159+
const result = await getRequestData(req);
160+
assert.ok(
161+
!(result instanceof Response),
162+
`Expected RenderOptions but got Response with status ${result instanceof Response ? result.status : 'N/A'} — inherited 'slots' should not trigger rejection`,
163+
);
164+
} finally {
165+
delete Object.prototype.slots;
166+
}
167+
});
168+
169+
it('only checks own properties for `componentExport` validation', async () => {
170+
// Temporarily pollute Object.prototype to simulate inherited properties
171+
Object.prototype.componentExport = 'default';
172+
try {
173+
const req = makePostRequest({
174+
encryptedComponentExport: 'encExport',
175+
encryptedProps: 'encProps',
176+
encryptedSlots: 'encSlots',
177+
});
178+
const result = await getRequestData(req);
179+
assert.ok(
180+
!(result instanceof Response),
181+
`Expected RenderOptions but got Response with status ${result instanceof Response ? result.status : 'N/A'} — inherited 'componentExport' should not trigger rejection`,
182+
);
183+
} finally {
184+
delete Object.prototype.componentExport;
185+
}
186+
});
149187
});
150188
// #endregion
151189

0 commit comments

Comments
 (0)