Skip to content

Commit

Permalink
fix: properly escape JSON for usage as an object literal inside of a …
Browse files Browse the repository at this point in the history
…script tag
  • Loading branch information
MatthewPattell committed Oct 28, 2024
1 parent 0209089 commit 1c7ffa4
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 8 deletions.
28 changes: 22 additions & 6 deletions __tests__/manager-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ describe('ManagerStream', () => {

const result = managerStream.take('suspenseId');

expect(result).to.include('<script>!window.mbxM && (window.mbxM = []);</script>');
expect(result).to.include(
'<script>window.mbxM.push({"store1":{"data":"value1"},"store2":{"data":"value2"}});</script>',
expect(result).to.equal(
'<script>!window.mbxM && (window.mbxM = []);</script><script>window.mbxM.push(JSON.parse("{\\"store1\\":{\\"data\\":\\"value1\\"},\\"store2\\":{\\"data\\":\\"value2\\"}}"));</script>',
);
});

Expand All @@ -50,9 +49,26 @@ describe('ManagerStream', () => {
managerStream.take('suspenseId'); // first call
const result = managerStream.take('suspenseId'); // second call

expect(result).to.not.include('<script>!window.mbxM && (window.mbxM = []);</script>');
expect(result).to.include(
'<script>window.mbxM.push({"store1":{"data":"value1"},"store2":{"data":"value2"}});</script>',
expect(result).to.equal(
'<script>window.mbxM.push(JSON.parse("{\\"store1\\":{\\"data\\":\\"value1\\"},\\"store2\\":{\\"data\\":\\"value2\\"}}"));</script>',
);
});

it('should properly escape JSON for usage as an object literal inside of a script tag', () => {
const storesIds = new Set(['store1', 'store2']);
const managerStream = new ManagerStream(manager as unknown as Manager);

manager.getSuspenseRelations.returns(new Map([['suspenseId', storesIds]]));
manager.toJSON.returns({
store1: { data: 'value1' },
store2: { data: '</script><script>console.log("Bad thing")</script>' },
});

managerStream.take('suspenseId'); // first call
const result = managerStream.take('suspenseId'); // second call

expect(result).to.equal(
'<script>window.mbxM.push(JSON.parse("{\\"store1\\":{\\"data\\":\\"value1\\"},\\"store2\\":{\\"data\\":\\"\\u003c/script\\u003e\\u003cscript\\u003econsole.log(\\\\\\"Bad thing\\\\\\")\\u003c/script\\u003e\\"}}"));</script>',
);
});
});
23 changes: 21 additions & 2 deletions src/manager-stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type Manager from './manager';

const ESCAPE_LOOKUP: { [match: string]: string } = {
'&': '\\u0026',
'>': '\\u003e',
'<': '\\u003c',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
};
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;

/**
* Stream mobx manager stores
*/
Expand All @@ -21,6 +30,14 @@ class ManagerStream {
this.manager = manager;
}

/**
* This utility is based on https://github.com/zertosh/htmlescape
* License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
*/
private htmlEscape(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
}

/**
* Return script with suspense stores to push on stream
*/
Expand All @@ -31,7 +48,9 @@ class ManagerStream {
return;
}

const storesState = JSON.stringify(this.manager.toJSON([...storesIds]));
const storesState = this.htmlEscape(
JSON.stringify(JSON.stringify(this.manager.toJSON([...storesIds]))),
);
const chunk = this.isPreamblePushed
? ''
: '<script>!window.mbxM && (window.mbxM = []);</script>';
Expand All @@ -40,7 +59,7 @@ class ManagerStream {
this.isPreamblePushed = true;
}

return `${chunk}<script>window.mbxM.push(${storesState});</script>`;
return `${chunk}<script>window.mbxM.push(JSON.parse(${storesState}));</script>`;
}
}

Expand Down

0 comments on commit 1c7ffa4

Please sign in to comment.