Skip to content

Commit e7d83ea

Browse files
authored
fix(chrome-devtool): implement sanitizePostMessagePayload to handle unsafe values in post messages (module-federation#4600)
1 parent b202ed6 commit e7d83ea

7 files changed

Lines changed: 232 additions & 21 deletions

File tree

.changeset/fuzzy-geese-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@module-federation/devtools": patch
3+
---
4+
5+
fix(chrome-devtool): Avoid message crashes in devtools by converting functions and other unsafe values into safe placeholders before forwarding module data.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { sanitizePostMessagePayload } from '../src/utils/chrome/safe-post-message';
4+
5+
describe('sanitizePostMessagePayload', () => {
6+
it('replaces functions and unsupported values with safe content', () => {
7+
const source = {
8+
fn: () => 'ok',
9+
nested: {
10+
value: 1,
11+
handler: function namedHandler() {
12+
return true;
13+
},
14+
},
15+
list: [undefined, Symbol('token'), 1n],
16+
error: new Error('boom'),
17+
regex: /mf/g,
18+
};
19+
20+
expect(sanitizePostMessagePayload(source)).toMatchObject({
21+
fn: 'function(){}',
22+
nested: {
23+
value: 1,
24+
handler: 'function(){}',
25+
},
26+
list: ['[undefined]', 'Symbol(token)', '1'],
27+
error: {
28+
name: 'Error',
29+
message: 'boom',
30+
},
31+
regex: '/mf/g',
32+
});
33+
});
34+
35+
it('handles circular data without throwing', () => {
36+
const source: Record<string, unknown> = {
37+
name: 'root',
38+
};
39+
source.self = source;
40+
source.map = new Map([['self', source]]);
41+
source.set = new Set([source]);
42+
43+
expect(sanitizePostMessagePayload(source)).toEqual({
44+
name: 'root',
45+
self: '[circular]',
46+
map: [['self', '[circular]']],
47+
set: ['[circular]'],
48+
});
49+
});
50+
});

packages/chrome-devtools/src/utils/chrome/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { GlobalModuleInfo } from '@module-federation/sdk';
22
import { FormID } from '../../template/constant';
33
import { definePropertyGlobalVal } from '../sdk';
4+
import { sanitizePostMessagePayload } from './safe-post-message';
45

56
export * from './storage';
67

@@ -108,8 +109,8 @@ export const getGlobalModuleInfo = async (
108109
) => {
109110
if (typeof window !== 'undefined' && window.__FEDERATION__?.moduleInfo) {
110111
callback(
111-
JSON.parse(
112-
JSON.stringify(window.__FEDERATION__?.moduleInfo),
112+
sanitizePostMessagePayload(
113+
window.__FEDERATION__?.moduleInfo,
113114
) as GlobalModuleInfo,
114115
);
115116
}
@@ -125,8 +126,8 @@ export const getGlobalModuleInfo = async (
125126
definePropertyGlobalVal(window, '__FEDERATION__', {});
126127
definePropertyGlobalVal(window, '__VMOK__', window.__FEDERATION__);
127128
}
128-
window.__FEDERATION__.originModuleInfo = JSON.parse(
129-
JSON.stringify(data?.moduleInfo),
129+
window.__FEDERATION__.originModuleInfo = sanitizePostMessagePayload(
130+
data?.moduleInfo,
130131
);
131132
if (data?.updateModule) {
132133
const moduleIds = Object.keys(window.__FEDERATION__.originModuleInfo);
@@ -145,10 +146,10 @@ export const getGlobalModuleInfo = async (
145146
}
146147
}
147148
if (data?.share) {
148-
window.__FEDERATION__.__SHARE__ = data.share;
149+
window.__FEDERATION__.__SHARE__ = sanitizePostMessagePayload(data.share);
149150
}
150-
window.__FEDERATION__.moduleInfo = JSON.parse(
151-
JSON.stringify(window.__FEDERATION__.originModuleInfo),
151+
window.__FEDERATION__.moduleInfo = sanitizePostMessagePayload(
152+
window.__FEDERATION__.originModuleInfo,
152153
);
153154
console.log('getGlobalModuleInfo window', window.__FEDERATION__);
154155
callback(window.__FEDERATION__.moduleInfo);

packages/chrome-devtools/src/utils/chrome/post-message-listener.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { sanitizePostMessagePayload } from './safe-post-message';
2+
13
if (window.moduleHandler) {
24
window.removeEventListener('message', window.moduleHandler);
35
} else {
@@ -10,11 +12,11 @@ if (window.moduleHandler) {
1012
chrome.runtime
1113
.sendMessage({
1214
origin,
13-
data: {
15+
data: sanitizePostMessagePayload({
1416
moduleInfo: data.moduleInfo,
1517
updateModule: data.updateModule,
1618
share: data.share,
17-
},
19+
}),
1820
})
1921
.catch(() => {
2022
return false;
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1+
import { sanitizePostMessagePayload } from './safe-post-message';
2+
13
// The purpose of this script is: the global plug-in injection is very early, the post message sends the message, the devtools receives the message listener is not running, resulting in the initial data is not available
24
// To get the initial data, actively get the global variable to send the message
35
const moduleInfo = window?.__FEDERATION__?.moduleInfo;
46
window.postMessage(
5-
{
7+
sanitizePostMessagePayload({
68
moduleInfo,
7-
share: JSON.parse(
8-
JSON.stringify(window?.__FEDERATION__?.__SHARE__, (_key, value) => {
9-
if (typeof value === 'function') {
10-
return 'Function';
11-
}
12-
return value;
13-
}),
14-
),
15-
},
9+
share: window?.__FEDERATION__?.__SHARE__,
10+
}),
1611
'*',
1712
);

packages/chrome-devtools/src/utils/chrome/post-message.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import helpers from '@module-federation/runtime/helpers';
22
import type { ModuleFederationRuntimePlugin } from '@module-federation/runtime';
33

44
import { definePropertyGlobalVal } from '../sdk';
5+
import { sanitizePostMessagePayload } from './safe-post-message';
56

67
type LoadRemoteSnapshotArgs = Parameters<
78
NonNullable<ModuleFederationRuntimePlugin['loadRemoteSnapshot']>
@@ -20,10 +21,10 @@ const getModuleInfo = (): ModuleFederationRuntimePlugin => {
2021

2122
if (!options || options.inBrowser) {
2223
window.postMessage(
23-
{
24+
sanitizePostMessagePayload({
2425
moduleInfo: globalSnapshot,
2526
updateModule: moduleInfo,
26-
},
27+
}),
2728
'*',
2829
);
2930
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
const FUNCTION_PLACEHOLDER = 'function(){}';
2+
const UNDEFINED_PLACEHOLDER = '[undefined]';
3+
const CIRCULAR_PLACEHOLDER = '[circular]';
4+
const NON_SERIALIZABLE_PLACEHOLDER = '[unserializable]';
5+
6+
const toStringTag = (value: unknown) =>
7+
Object.prototype.toString.call(value).slice(8, -1);
8+
9+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
10+
if (!value || typeof value !== 'object') {
11+
return false;
12+
}
13+
14+
const proto = Object.getPrototypeOf(value);
15+
return proto === Object.prototype || proto === null;
16+
};
17+
18+
const sanitizeValue = (
19+
value: unknown,
20+
seen: WeakMap<object, unknown>,
21+
): unknown => {
22+
if (
23+
value === null ||
24+
typeof value === 'string' ||
25+
typeof value === 'number'
26+
) {
27+
return Number.isNaN(value) ? '[NaN]' : value;
28+
}
29+
30+
if (typeof value === 'boolean') {
31+
return value;
32+
}
33+
34+
if (typeof value === 'function') {
35+
return FUNCTION_PLACEHOLDER;
36+
}
37+
38+
if (typeof value === 'undefined') {
39+
return UNDEFINED_PLACEHOLDER;
40+
}
41+
42+
if (typeof value === 'bigint' || typeof value === 'symbol') {
43+
return String(value);
44+
}
45+
46+
if (!(value instanceof Object)) {
47+
return NON_SERIALIZABLE_PLACEHOLDER;
48+
}
49+
50+
if (seen.has(value)) {
51+
return CIRCULAR_PLACEHOLDER;
52+
}
53+
54+
if (Array.isArray(value)) {
55+
const next: unknown[] = [];
56+
seen.set(value, next);
57+
value.forEach((item, index) => {
58+
next[index] = sanitizeValue(item, seen);
59+
});
60+
return next;
61+
}
62+
63+
if (value instanceof Date) {
64+
return value.toISOString();
65+
}
66+
67+
if (value instanceof RegExp) {
68+
return value.toString();
69+
}
70+
71+
if (value instanceof Error) {
72+
const next = {
73+
name: value.name,
74+
message: value.message,
75+
stack: value.stack || '',
76+
};
77+
seen.set(value, next);
78+
return next;
79+
}
80+
81+
if (value instanceof Map) {
82+
const next: unknown[] = [];
83+
seen.set(value, next);
84+
next.push(
85+
...Array.from(value.entries()).map(([key, item]) => [
86+
sanitizeValue(key, seen),
87+
sanitizeValue(item, seen),
88+
]),
89+
);
90+
return next;
91+
}
92+
93+
if (value instanceof Set) {
94+
const next: unknown[] = [];
95+
seen.set(value, next);
96+
next.push(
97+
...Array.from(value.values()).map((item) => sanitizeValue(item, seen)),
98+
);
99+
return next;
100+
}
101+
102+
if (ArrayBuffer.isView(value)) {
103+
return Array.from(new Uint8Array(value.buffer));
104+
}
105+
106+
if (value instanceof ArrayBuffer) {
107+
return Array.from(new Uint8Array(value));
108+
}
109+
110+
if (typeof Node !== 'undefined' && value instanceof Node) {
111+
return `[${toStringTag(value)}]`;
112+
}
113+
114+
if (typeof Window !== 'undefined' && value instanceof Window) {
115+
return '[Window]';
116+
}
117+
118+
if (typeof Document !== 'undefined' && value instanceof Document) {
119+
return '[Document]';
120+
}
121+
122+
const next: Record<string, unknown> = {};
123+
seen.set(value, next);
124+
125+
const entries = isPlainObject(value)
126+
? Object.keys(value).map((key) => {
127+
try {
128+
return [key, value[key]] as const;
129+
} catch (_error) {
130+
return [key, NON_SERIALIZABLE_PLACEHOLDER] as const;
131+
}
132+
})
133+
: Reflect.ownKeys(value).map((key) => {
134+
try {
135+
return [
136+
String(key),
137+
(value as Record<PropertyKey, unknown>)[key],
138+
] as const;
139+
} catch (_error) {
140+
return [String(key), NON_SERIALIZABLE_PLACEHOLDER] as const;
141+
}
142+
});
143+
144+
entries.forEach(([key, item]) => {
145+
try {
146+
next[key] = sanitizeValue(item, seen);
147+
} catch (_error) {
148+
next[key] = NON_SERIALIZABLE_PLACEHOLDER;
149+
}
150+
});
151+
152+
return next;
153+
};
154+
155+
export const sanitizePostMessagePayload = <T>(payload: T): T => {
156+
return sanitizeValue(payload, new WeakMap<object, unknown>()) as T;
157+
};

0 commit comments

Comments
 (0)