Skip to content

Commit fccf33e

Browse files
committed
fix(security): bind iframe messages and token replies to trusted peers (LUNA-003)
1 parent 065a6d1 commit fccf33e

3 files changed

Lines changed: 62 additions & 8 deletions

File tree

‎scripts/security-checks.mjs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,35 @@ assert.equal(interpolate('{{jms_null}}', {jms_null: null}), '');
3232
assert.throws(() => interpolate('{{constructor}}', {}));
3333
assert.throws(() => interpolate('{{jms_missing}}', {}));
3434
console.log('Command interpolation controls passed (no evaluation or recursive substitution)');
35+
36+
const frameJS = ts.transpileModule(fs.readFileSync('src/app/elements/iframe/iframe.component.ts', 'utf8'), {
37+
compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true}
38+
}).outputText;
39+
class Emitter {events = []; emit(value) {this.events.push(value);}}
40+
class Subject {pipe() {return {subscribe() {}};} next() {}}
41+
const decorator = () => () => {};
42+
const angular = {EventEmitter: Emitter, Component: decorator, Input: decorator, Output: decorator, ViewChild: decorator};
43+
const frameSandbox = {exports: {}, URL, window: {location: {href: 'https://luna.example/luna/'}},
44+
require(name) {if (name === '@angular/core') return angular; if (name === 'rxjs') return {Subject}; return {};}};
45+
vm.runInNewContext(frameJS, frameSandbox);
46+
const Frame = frameSandbox.exports.ElementIframeComponent;
47+
const frame = new Frame();
48+
const sent = [];
49+
const peer = {postMessage(message, origin) {sent.push({message, origin});}};
50+
frame.src = 'https://koko.example:8443/koko/connect/?token=secret';
51+
frame.id = 'window-test';
52+
frame.view = {};
53+
frame.iframeRef = {nativeElement: {contentWindow: peer}};
54+
frame.handleIframeEvent = () => {};
55+
frame.ngAfterViewInit();
56+
for (const e of [
57+
{source: {}, origin: 'https://koko.example:8443'},
58+
{source: peer, origin: 'https://attacker.example'},
59+
{source: peer, origin: 'https://koko.example'},
60+
]) frame.eventHandler({...e, data: {id: frame.id, name: 'CREATE_FILE_CONNECT_TOKEN'}});
61+
assert.equal(frame.createFileConnectToken.events.length, 0);
62+
frame.eventHandler({source: peer, origin: 'https://koko.example:8443', data: {id: frame.id, name: 'CREATE_FILE_CONNECT_TOKEN'}});
63+
assert.equal(frame.createFileConnectToken.events.length, 1);
64+
frame.eventHandler({source: peer, origin: 'https://koko.example:8443', data: {id: frame.id, name: 'PING'}});
65+
assert.equal(sent[0].origin, 'https://koko.example:8443');
66+
console.log('Iframe message source/origin/port and outgoing target controls passed');

‎src/app/elements/content/content-window/koko/koko.component.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,18 @@ export class ElementConnectorKokoComponent implements OnInit {
6868

6969
async createFileConnectToken(evt) {
7070
const iframeWindow = (this.iframeRef as unknown as { iframeWindow: Window }).iframeWindow;
71+
const targetURL = this.iframeURL;
72+
const targetOrigin = new URL(targetURL, window.location.href).origin;
7173
const oldConnectToken = this.view.connectToken;
7274
const newConnectToken = await this._connectTokenSvc.exchange(oldConnectToken);
7375

74-
if (!newConnectToken) {
76+
if (!newConnectToken || this.iframeURL !== targetURL) {
7577
return;
7678
}
7779

7880
iframeWindow.postMessage(
7981
{ name: 'GET_FILE_CONNECT_TOKEN', token: newConnectToken.id },
80-
'*'
82+
targetOrigin
8183
);
8284
}
8385

‎src/app/elements/iframe/iframe.component.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,19 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy
8888
this.view.iframeElement = this.iframeWindow;
8989

9090
this.eventHandler = function (e: any) {
91+
if (e.source !== this.iframeWindow || e.origin !== this.getIframeOrigin()) {
92+
return;
93+
}
9194
const msg = e.data;
95+
if (!msg || typeof msg !== 'object') return;
9296

9397
if (msg.id !== this.id) {
9498
return;
9599
}
96100

97101
switch (msg.name) {
98102
case 'PING': {
99-
this.iframeWindow.postMessage({ name: 'PONG', id: this.id }, '*');
103+
this.postToIframe({ name: 'PONG', id: this.id });
100104
break;
101105
}
102106
case 'PONG':
@@ -166,14 +170,30 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy
166170
this.handleIframeEvent();
167171
}
168172

173+
private getIframeOrigin(): string | null {
174+
try {
175+
const url = new URL(this.src, window.location.href);
176+
return ['http:', 'https:'].includes(url.protocol) ? url.origin : null;
177+
} catch {
178+
return null;
179+
}
180+
}
181+
182+
private postToIframe(message: unknown): void {
183+
const origin = this.getIframeOrigin();
184+
if (origin && this.iframeWindow) {
185+
this.iframeWindow.postMessage(message, origin);
186+
}
187+
}
188+
169189
ngOnDestroy() {
170190
window.removeEventListener('message', this.eventHandler);
171191
this.subscription.unsubscribe();
172192
}
173193

174194
setActive() {
175195
this._logger.debug(`[Luna] Send FOCUS to: ${this.id}`);
176-
this.iframeWindow.postMessage({ name: 'FOCUS' }, '*');
196+
this.postToIframe({ name: 'FOCUS' });
177197
}
178198

179199
handleIframeEvent() {
@@ -187,7 +207,7 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy
187207
// @ts-ignore
188208
this.ping = setInterval(() => {
189209
this._logger.info(`[Luna] Send PING to: ${this.id}`);
190-
this.iframeWindow.postMessage({ name: 'PING', id: this.id, disbaleFileManager }, '*');
210+
this.postToIframe({ name: 'PING', id: this.id, disbaleFileManager });
191211
}, 500);
192212

193213
// 30s 内未PING通, 则主动关闭
@@ -201,15 +221,15 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy
201221

202222
this.subscription = this._iframeSvc.message$.subscribe(message => {
203223
// this._logger.info('[Luna] Send msg to iframe: ', message);
204-
this.iframeWindow.postMessage(message, '*');
224+
this.postToIframe(message);
205225
});
206226

207227
window.addEventListener('message', this.eventHandler);
208228
}
209229

210230
sendCommand(data) {
211231
this._logger.info(`[Luna] Send CMD to: ${this.id}`);
212-
this.iframeWindow.postMessage({ name: 'CMD', data: data.data }, '*');
232+
this.postToIframe({ name: 'CMD', data: data.data });
213233
}
214234

215235
// 没有位置用啊
@@ -252,7 +272,7 @@ export class ElementIframeComponent implements OnInit, AfterViewInit, OnDestroy
252272

253273
sendInputActive() {
254274
this._logger.info(`[Luna] Send Input_ACTIVE to: ${this.id}`);
255-
this.iframeWindow.postMessage({ name: 'INPUT_ACTIVE', data: "" }, '*');
275+
this.postToIframe({ name: 'INPUT_ACTIVE', data: "" });
256276
}
257277

258278
sendInputActiveToOtherViews() {

0 commit comments

Comments
 (0)