Skip to content

Commit 103378b

Browse files
authored
Warn for javascript: URLs in DOM sinks (#15047)
* Prevent javascript protocol URLs * Just warn when disableJavaScriptURLs is false This avoids a breaking change. * Allow framesets * Allow <html> to be used in integration tests Full document renders requires server rendering so the client path just uses the hydration path in this case to simplify writing these tests. * Detect leading and intermediate characters and test mixed case These are considered valid javascript urls by browser so they must be included in the filter. This is an exact match according to the spec but maybe we should include a super set to be safer? * Test updates to ensure we have coverage there too * Fix toString invocation and Flow types Right now we invoke toString twice when we hydrate (three times with the flag off). Ideally we should only do it once even in this case but the code structure doesn't really allow for that right now. * s/itRejects/itRejectsRendering * Dedupe warning and add the unsafe URL to the warning message * Add test that fails if g is added to the sanitizer This only affects the prod version since the warning is deduped anyway. * Fix prod test
1 parent 5d0c3c6 commit 103378b

14 files changed

Lines changed: 451 additions & 22 deletions
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
/* eslint-disable no-script-url */
11+
12+
'use strict';
13+
14+
constReactDOMServerIntegrationUtils=require('./utils/ReactDOMServerIntegrationTestUtils');
15+
16+
letReact;
17+
letReactDOM;
18+
letReactDOMServer;
19+
20+
functionrunTests(itRenders,itRejectsRendering,expectToReject){
21+
itRenders('a http link with the word javascript in it',asyncrender=>{
22+
conste=awaitrender(
23+
<ahref="http://javascript:0/thisisfine">Click me</a>,
24+
);
25+
expect(e.tagName).toBe('A');
26+
expect(e.href).toBe('http://javascript:0/thisisfine');
27+
});
28+
29+
itRejectsRendering('a javascript protocol href',asyncrender=>{
30+
// Only the first one warns. The second warning is deduped.
31+
conste=awaitrender(
32+
<div>
33+
<ahref="javascript:notfine">p0wned</a>
34+
<ahref="javascript:notfineagain">p0wned again</a>
35+
</div>,
36+
1,
37+
);
38+
expect(e.firstChild.href).toBe('javascript:notfine');
39+
expect(e.lastChild.href).toBe('javascript:notfineagain');
40+
});
41+
42+
itRejectsRendering(
43+
'a javascript protocol with leading spaces',
44+
asyncrender=>{
45+
conste=awaitrender(
46+
<ahref={' \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
47+
1,
48+
);
49+
// We use an approximate comparison here because JSDOM might not parse
50+
// \u0000 in HTML properly.
51+
expect(e.href).toContain('notfine');
52+
},
53+
);
54+
55+
itRejectsRendering(
56+
'a javascript protocol with intermediate new lines and mixed casing',
57+
asyncrender=>{
58+
conste=awaitrender(
59+
<ahref={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
60+
1,
61+
);
62+
expect(e.href).toBe('javascript:notfine');
63+
},
64+
);
65+
66+
itRejectsRendering('a javascript protocol area href',asyncrender=>{
67+
conste=awaitrender(
68+
<map>
69+
<areahref="javascript:notfine"/>
70+
</map>,
71+
1,
72+
);
73+
expect(e.firstChild.href).toBe('javascript:notfine');
74+
});
75+
76+
itRejectsRendering('a javascript protocol form action',asyncrender=>{
77+
conste=awaitrender(<formaction="javascript:notfine">p0wned</form>,1);
78+
expect(e.action).toBe('javascript:notfine');
79+
});
80+
81+
itRejectsRendering(
82+
'a javascript protocol button formAction',
83+
asyncrender=>{
84+
conste=awaitrender(<inputformAction="javascript:notfine"/>,1);
85+
expect(e.getAttribute('formAction')).toBe('javascript:notfine');
86+
},
87+
);
88+
89+
itRejectsRendering('a javascript protocol input formAction',asyncrender=>{
90+
conste=awaitrender(
91+
<buttonformAction="javascript:notfine">p0wned</button>,
92+
1,
93+
);
94+
expect(e.getAttribute('formAction')).toBe('javascript:notfine');
95+
});
96+
97+
itRejectsRendering('a javascript protocol iframe src',asyncrender=>{
98+
conste=awaitrender(<iframesrc="javascript:notfine"/>,1);
99+
expect(e.src).toBe('javascript:notfine');
100+
});
101+
102+
itRejectsRendering('a javascript protocol frame src',asyncrender=>{
103+
conste=awaitrender(
104+
<html>
105+
<head/>
106+
<frameset>
107+
<framesrc="javascript:notfine"/>
108+
</frameset>
109+
</html>,
110+
1,
111+
);
112+
expect(e.lastChild.firstChild.src).toBe('javascript:notfine');
113+
});
114+
115+
itRejectsRendering('a javascript protocol in an SVG link',asyncrender=>{
116+
conste=awaitrender(
117+
<svg>
118+
<ahref="javascript:notfine"/>
119+
</svg>,
120+
1,
121+
);
122+
expect(e.firstChild.getAttribute('href')).toBe('javascript:notfine');
123+
});
124+
125+
itRejectsRendering(
126+
'a javascript protocol in an SVG link with a namespace',
127+
asyncrender=>{
128+
conste=awaitrender(
129+
<svg>
130+
<axlinkHref="javascript:notfine"/>
131+
</svg>,
132+
1,
133+
);
134+
expect(
135+
e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink','href'),
136+
).toBe('javascript:notfine');
137+
},
138+
);
139+
140+
it('rejects a javascript protocol href if it is added during an update',()=>{
141+
letcontainer=document.createElement('div');
142+
ReactDOM.render(<ahref="thisisfine">click me</a>,container);
143+
expectToReject(()=>{
144+
ReactDOM.render(<ahref="javascript:notfine">click me</a>,container);
145+
});
146+
});
147+
}
148+
149+
describe('ReactDOMServerIntegration - Untrusted URLs',()=>{
150+
functioninitModules(){
151+
jest.resetModuleRegistry();
152+
React=require('react');
153+
ReactDOM=require('react-dom');
154+
ReactDOMServer=require('react-dom/server');
155+
156+
// Make them available to the helpers.
157+
return{
158+
ReactDOM,
159+
ReactDOMServer,
160+
};
161+
}
162+
163+
const{resetModules, itRenders}=ReactDOMServerIntegrationUtils(initModules);
164+
165+
beforeEach(()=>{
166+
resetModules();
167+
});
168+
169+
runTests(itRenders,itRenders,fn=>
170+
expect(fn).toWarnDev(
171+
'Warning: A future version of React will block javascript: URLs as a security precaution. '+
172+
'Use event handlers instead if you can. If you need to generate unsafe HTML try using '+
173+
'dangerouslySetInnerHTML instead. React was passed "javascript:notfine".\n'+
174+
' in a (at **)',
175+
),
176+
);
177+
});
178+
179+
describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs',()=>{
180+
functioninitModules(){
181+
jest.resetModuleRegistry();
182+
constReactFeatureFlags=require('shared/ReactFeatureFlags');
183+
ReactFeatureFlags.disableJavaScriptURLs=true;
184+
185+
React=require('react');
186+
ReactDOM=require('react-dom');
187+
ReactDOMServer=require('react-dom/server');
188+
189+
// Make them available to the helpers.
190+
return{
191+
ReactDOM,
192+
ReactDOMServer,
193+
};
194+
}
195+
196+
const{
197+
resetModules,
198+
itRenders,
199+
itThrowsWhenRendering,
200+
clientRenderOnBadMarkup,
201+
clientRenderOnServerString,
202+
}=ReactDOMServerIntegrationUtils(initModules);
203+
204+
constexpectToReject=fn=>{
205+
letmsg;
206+
try{
207+
fn();
208+
}catch(x){
209+
msg=x.message;
210+
}
211+
expect(msg).toContain(
212+
'React has blocked a javascript: URL as a security precaution.',
213+
);
214+
};
215+
216+
beforeEach(()=>{
217+
resetModules();
218+
});
219+
220+
runTests(
221+
itRenders,
222+
(message,test)=>
223+
itThrowsWhenRendering(message,test,'blocked a javascript: URL'),
224+
expectToReject,
225+
);
226+
227+
itRenders('only the first invocation of toString',asyncrender=>{
228+
letexpectedToStringCalls=1;
229+
if(render===clientRenderOnBadMarkup){
230+
// It gets called once on the server and once on the client
231+
// which happens to share the same object in our test runner.
232+
expectedToStringCalls=2;
233+
}
234+
if(render===clientRenderOnServerString&&__DEV__){
235+
// The hydration validation calls it one extra time.
236+
// TODO: It would be good if we only called toString once for
237+
// consistency but the code structure makes that hard right now.
238+
expectedToStringCalls=2;
239+
}
240+
241+
lettoStringCalls=0;
242+
letfirstIsSafe={
243+
toString(){
244+
// This tries to avoid the validation by pretending to be safe
245+
// the first times it is called and then becomes dangerous.
246+
toStringCalls++;
247+
if(toStringCalls<=expectedToStringCalls){
248+
return'https://fb.me/';
249+
}
250+
return'javascript:notfine';
251+
},
252+
};
253+
254+
conste=awaitrender(<ahref={firstIsSafe}/>);
255+
expect(toStringCalls).toBe(expectedToStringCalls);
256+
expect(e.href).toBe('https://fb.me/');
257+
});
258+
259+
it('rejects a javascript protocol href if it is added during an update twice',()=>{
260+
letcontainer=document.createElement('div');
261+
ReactDOM.render(<ahref="thisisfine">click me</a>,container);
262+
expectToReject(()=>{
263+
ReactDOM.render(<ahref="javascript:notfine">click me</a>,container);
264+
});
265+
// The second update ensures that a global flag hasn't been added to the regex
266+
// which would fail to match the second time it is called.
267+
expectToReject(()=>{
268+
ReactDOM.render(<ahref="javascript:notfine">click me</a>,container);
269+
});
270+
});
271+
});

0 commit comments

Comments
 (0)