diff options
author | 2024-07-19 07:02:14 -0700 | |
---|---|---|
committer | 2024-07-19 15:02:14 +0100 | |
commit | ca335e1dc09bc83d3f8f5b9dd54f116bcb4881e4 (patch) | |
tree | fd7d49f04a5dc1a03c506c65d7d64e80a4047233 | |
parent | 026e8baf3323e99f96530999fd32a0a9b305854d (diff) | |
download | astro-ca335e1dc09bc83d3f8f5b9dd54f116bcb4881e4.tar.gz astro-ca335e1dc09bc83d3f8f5b9dd54f116bcb4881e4.tar.zst astro-ca335e1dc09bc83d3f8f5b9dd54f116bcb4881e4.zip |
Fix an XSS in Server Islands. (#11508)
* Fix an XSS in Server Islands.
Discussed with @FredKSchott that this is OK to disclose since Server Islands are still experimental.
It's generally not safe to use `JSON.stringify` to interpolate potentially attacker controlled data into `<script>` tags as JSON doesn't escape `<>"'` and so one can use it to break out of the script tag and e.g. make a new one with controlled content.
See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss
* Format
* Create smart-snakes-promise.md
* Switch to manual encoding
---------
Co-authored-by: Matt Kane <m@mk.gg>
-rw-r--r-- | .changeset/smart-snakes-promise.md | 5 | ||||
-rw-r--r-- | packages/astro/src/runtime/server/render/server-islands.ts | 17 |
2 files changed, 18 insertions, 4 deletions
diff --git a/.changeset/smart-snakes-promise.md b/.changeset/smart-snakes-promise.md new file mode 100644 index 000000000..e46353e6a --- /dev/null +++ b/.changeset/smart-snakes-promise.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Escapes HTML in serialized props diff --git a/packages/astro/src/runtime/server/render/server-islands.ts b/packages/astro/src/runtime/server/render/server-islands.ts index ee6b03ae2..52b6b006e 100644 --- a/packages/astro/src/runtime/server/render/server-islands.ts +++ b/packages/astro/src/runtime/server/render/server-islands.ts @@ -14,6 +14,15 @@ export function containsServerDirective(props: Record<string | number, any>) { return 'server:component-directive' in props; } +function safeJsonStringify(obj: any) { + return JSON.stringify(obj) + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') + .replace(/</g, '\\u003c') + .replace(/>/g, '\\u003e') + .replace(/\//g, '\\u002f'); +} + export function renderServerIsland( result: SSRResult, _displayName: string, @@ -53,13 +62,13 @@ export function renderServerIsland( const hostId = crypto.randomUUID(); destination.write(`<script async type="module" data-island-id="${hostId}"> -let componentId = ${JSON.stringify(componentId)}; -let componentExport = ${JSON.stringify(componentExport)}; +let componentId = ${safeJsonStringify(componentId)}; +let componentExport = ${safeJsonStringify(componentExport)}; let script = document.querySelector('script[data-island-id="${hostId}"]'); let data = { componentExport, - props: ${JSON.stringify(props)}, - slots: ${JSON.stringify(renderedSlots)}, + props: ${safeJsonStringify(props)}, + slots: ${safeJsonStringify(renderedSlots)}, }; let response = await fetch('/_server-islands/${componentId}', { |