11const workspaceShareParam = 'sws'
22const workspaceShareSchemaVersion = 1
33const workspaceShareCompression = 'gzip'
4+ const maxWorkspaceShareEncodedPayloadLength = 8192
5+ const maxWorkspaceShareDecodedBytes = 1024 * 1024
6+ const maxWorkspaceShareExpansionRatio = 100
47
58const isNativeWorkspaceShareCodecSupported = ( ) => {
69 return (
@@ -53,6 +56,60 @@ const streamToUint8Array = async stream => {
5356 return new Uint8Array ( buffer )
5457}
5558
59+ const streamToUint8ArrayWithLimit = async ( {
60+ stream,
61+ maxBytes,
62+ compressedBytesLength,
63+ maxExpansionRatio,
64+ } ) => {
65+ const reader = stream . getReader ( )
66+ const chunks = [ ]
67+ let totalBytes = 0
68+
69+ try {
70+ while ( true ) {
71+ // Sequential reads are required for Web Streams reader consumption.
72+ // eslint-disable-next-line no-await-in-loop
73+ const { done, value } = await reader . read ( )
74+ if ( done ) {
75+ break
76+ }
77+
78+ if ( ! ( value instanceof Uint8Array ) ) {
79+ continue
80+ }
81+
82+ totalBytes += value . byteLength
83+ if ( totalBytes > maxBytes ) {
84+ throw new Error ( 'Workspace share payload exceeds maximum decoded size.' )
85+ }
86+
87+ if (
88+ typeof compressedBytesLength === 'number' &&
89+ compressedBytesLength > 0 &&
90+ typeof maxExpansionRatio === 'number' &&
91+ maxExpansionRatio > 0 &&
92+ totalBytes > compressedBytesLength * maxExpansionRatio
93+ ) {
94+ throw new Error ( 'Workspace share payload expansion ratio is too large.' )
95+ }
96+
97+ chunks . push ( value )
98+ }
99+ } finally {
100+ reader . releaseLock ( )
101+ }
102+
103+ const bytes = new Uint8Array ( totalBytes )
104+ let offset = 0
105+ for ( const chunk of chunks ) {
106+ bytes . set ( chunk , offset )
107+ offset += chunk . byteLength
108+ }
109+
110+ return bytes
111+ }
112+
56113const compressText = async text => {
57114 const encoder = new TextEncoder ( )
58115 const sourceBytes = encoder . encode ( text )
@@ -69,7 +126,12 @@ const decompressText = async bytes => {
69126 const decompressedStream = sourceStream . pipeThrough (
70127 new DecompressionStream ( workspaceShareCompression ) ,
71128 )
72- const decompressedBytes = await streamToUint8Array ( decompressedStream )
129+ const decompressedBytes = await streamToUint8ArrayWithLimit ( {
130+ stream : decompressedStream ,
131+ maxBytes : maxWorkspaceShareDecodedBytes ,
132+ compressedBytesLength : bytes ?. byteLength ?? 0 ,
133+ maxExpansionRatio : maxWorkspaceShareExpansionRatio ,
134+ } )
73135 const decoder = new TextDecoder ( )
74136 return decoder . decode ( decompressedBytes )
75137}
@@ -92,7 +154,12 @@ const encodeWorkspaceSharePayload = async snapshot => {
92154
93155 const serialized = JSON . stringify ( envelope )
94156 const compressed = await compressText ( serialized )
95- return toBase64Url ( compressed )
157+ const encoded = toBase64Url ( compressed )
158+ if ( encoded . length > maxWorkspaceShareEncodedPayloadLength ) {
159+ throw new Error ( 'Workspace share payload is too large.' )
160+ }
161+
162+ return encoded
96163}
97164
98165const decodeWorkspaceSharePayload = async encodedPayload => {
@@ -104,6 +171,10 @@ const decodeWorkspaceSharePayload = async encodedPayload => {
104171 throw new TypeError ( 'Workspace share payload must be a non-empty string.' )
105172 }
106173
174+ if ( encodedPayload . trim ( ) . length > maxWorkspaceShareEncodedPayloadLength ) {
175+ throw new Error ( 'Workspace share payload exceeds maximum encoded length.' )
176+ }
177+
107178 let parsed = null
108179 try {
109180 const compressedBytes = fromBase64Url ( encodedPayload . trim ( ) )
0 commit comments