Skip to content

Commit 747e0bb

Browse files
authored
Update postMessage handler for embedded query page to verify origin (#1676)
1 parent 8bd092a commit 747e0bb

4 files changed

Lines changed: 58 additions & 12 deletions

File tree

docs_website/docs/configurations/infra_config.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Otherwise you can also pass the environment variable directly when launching the
4040

4141
### Iframe Embedding
4242

43-
`IFRAME_ALLOWED_ORIGINS`: This is the allowed list for embedding Querybook in an iframe. By default it can only be embedded by itself.
43+
`IFRAME_ALLOWED_ORIGINS`: This is the allowed list for embedding Querybook in an iframe. By default it can only be embedded by itself. When set, this also restricts which origins can send queries to the [embedded editor](/docs/integrations/embedded_iframe) via `postMessage`.
4444

4545
### Database
4646

querybook/server/datasources/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from app.flask_app import limiter
22
from app.datasource import register
3+
from env import QuerybookSettings
34
from lib.change_log import get_change_log_list, get_change_log_content_by_date
45

56

@@ -22,3 +23,8 @@ def test_ratelimit():
2223
Endpoint to ensure ratelimit works in prod
2324
"""
2425
return "yes"
26+
27+
28+
@register("/utils/embedded/allowed_origins/", methods=["GET"])
29+
def get_embedded_allowed_origins():
30+
return QuerybookSettings.IFRAME_ALLOWED_ORIGINS or []

querybook/webapp/components/EmbeddedQueryPage/EmbeddedQueryPage.tsx

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ import React from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33

44
import QueryComposer from 'components/QueryComposer/QueryComposer';
5+
import { useResource } from 'hooks/useResource';
56
import { IAdhocQuery } from 'const/adhocQuery';
67
import { receiveAdhocQuery } from 'redux/adhocQuery/action';
78
import { Dispatch, IStoreState } from 'redux/store/types';
9+
import { EmbeddedResource } from 'resource/embedded';
810
import { Button } from 'ui/Button/Button';
911
import { FullHeight } from 'ui/FullHeight/FullHeight';
1012

1113
import './EmbeddedQueryPage.scss';
1214

15+
function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {
16+
if (allowedOrigins.length === 0 || origin === window.location.origin) {
17+
return true;
18+
}
19+
20+
return allowedOrigins.some((allowed) => origin === new URL(allowed).origin);
21+
}
22+
1323
const EmbeddedQueryPage: React.FunctionComponent = () => {
1424
const environmentId = useSelector(
1525
(state: IStoreState) => state.environment.currentEnvironmentId
@@ -18,15 +28,30 @@ const EmbeddedQueryPage: React.FunctionComponent = () => {
1828
(state: IStoreState) => state.adhocQuery[environmentId]?.query ?? ''
1929
);
2030

31+
const { data: allowedOrigins } = useResource(
32+
EmbeddedResource.getAllowedOrigins
33+
);
34+
2135
const dispatch: Dispatch = useDispatch();
2236
const setQuery = React.useCallback(
23-
(query: IAdhocQuery) =>
24-
dispatch(receiveAdhocQuery(query, environmentId)),
25-
[]
37+
(adhocQuery: IAdhocQuery) =>
38+
dispatch(receiveAdhocQuery(adhocQuery, environmentId)),
39+
[dispatch, environmentId]
2640
);
2741

28-
const onMessage = React.useCallback((e) => {
29-
if (e.data && e.data.type === 'SET_QUERY') {
42+
const onMessage = React.useCallback(
43+
(e: MessageEvent) => {
44+
if (!e.data || e.data.type !== 'SET_QUERY') {
45+
return;
46+
}
47+
48+
if (!isOriginAllowed(e.origin, allowedOrigins)) {
49+
console.warn(
50+
`[EmbeddedQueryPage] Blocked postMessage from untrusted origin: ${e.origin}`
51+
);
52+
return;
53+
}
54+
3055
const query: IAdhocQuery = {};
3156
if (e.data.value) {
3257
query.query = e.data.value;
@@ -36,21 +61,30 @@ const EmbeddedQueryPage: React.FunctionComponent = () => {
3661
}
3762

3863
setQuery(query);
39-
}
40-
}, []);
64+
},
65+
[allowedOrigins, setQuery]
66+
);
4167

4268
React.useEffect(() => {
43-
// Tell the parent we are ready to receive query
69+
if (!allowedOrigins) {
70+
return;
71+
}
72+
73+
// Tell the parent we are ready to receive query after allowed origins are loaded
4474
window.parent.postMessage({ type: 'SEND_QUERY' }, '*');
45-
}, []);
75+
}, [allowedOrigins]);
4676

47-
// Setup query receiver
4877
React.useEffect(() => {
78+
if (!allowedOrigins) {
79+
return;
80+
}
81+
82+
// Attach event listener to receive query from parent
4983
window.addEventListener('message', onMessage, false);
5084
return () => {
5185
window.removeEventListener('message', onMessage);
5286
};
53-
}, [onMessage]);
87+
}, [allowedOrigins, onMessage]);
5488

5589
return (
5690
<FullHeight flex={'column'} className="EmbeddedQueryPage">
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import ds from 'lib/datasource';
2+
3+
export const EmbeddedResource = {
4+
getAllowedOrigins: () =>
5+
ds.fetch<string[]>(`/utils/embedded/allowed_origins/`),
6+
};

0 commit comments

Comments
 (0)