Skip to content

Commit

Permalink
escape hrefs in document queries
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Sep 30, 2022
1 parent 3f3f9d5 commit f1925a8
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 3 deletions.
28 changes: 25 additions & 3 deletions packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,11 @@ function createStyleResource(
}
}

const limitedEscacpedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
href,
);
const existingEl = ownerDocument.querySelector(
`link[rel="stylesheet"][href="${href}"]`,
`link[rel="stylesheet"][href="${limitedEscacpedHref}"]`,
);
const resource = {
type: 'style',
Expand Down Expand Up @@ -585,8 +588,11 @@ function createPreloadResource(
href: string,
props: PreloadProps,
): PreloadResource {
const limitedEscacpedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
href,
);
let element = ownerDocument.querySelector(
`link[rel="preload"][href="${href}"]`,
`link[rel="preload"][href="${limitedEscacpedHref}"]`,
);
if (!element) {
element = createResourceInstance('link', props, ownerDocument);
Expand All @@ -604,8 +610,11 @@ function createPreloadResource(
function acquireStyleResource(resource: StyleResource): Instance {
if (!resource.instance) {
const {props, ownerDocument, precedence} = resource;
const limitedEscacpedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
props.href,
);
const existingEl = ownerDocument.querySelector(
`link[rel="stylesheet"][data-rprec][href="${props.href}"]`,
`link[rel="stylesheet"][data-rprec][href="${limitedEscacpedHref}"]`,
);
if (existingEl) {
resource.instance = existingEl;
Expand Down Expand Up @@ -801,3 +810,16 @@ export function isHostResourceType(type: string, props: Props): boolean {
function isResourceAsType(as: mixed): boolean {
return as === 'style' || as === 'font';
}

// When passing user input into querySelector(All) the embedded string must not alter
// the semantics of the query. This escape function is safe to use when we know the
// provided value is going to be wrapped in double quotes as part of an attribute selector
// Do not use it anywhere else
// we escape double quotes and backslashes
const escapeSelectorAttributeValueInsideDoubleQuotesRegex = /[\"\\]/g;
function escapeSelectorAttributeValueInsideDoubleQuotes(value: string): string {
return value.replace(
escapeSelectorAttributeValueInsideDoubleQuotesRegex,
match => (match === '"' ? '\\"' : match === '\\' ? '\\\\' : ''),
);
}
113 changes: 113 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFloat-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3483,4 +3483,117 @@ describe('ReactDOMFloat', () => {
}
});
});

describe('escaping', () => {
// @gate enableFloat
it('escapes hrefs when selecting matching elements in the document when rendering Resources', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="preload" href="preload" as="style" />
<link rel="stylesheet" href="style" precedence="style" />
<link rel="stylesheet" href="with\slashes" precedence="style" />
<div id="container" />
</body>
</html>,
);
pipe(writable);
});

container = document.getElementById('container');
const root = ReactDOMClient.createRoot(container);
root.render(
<div>
<link rel="preload" href={'preload"][rel="preload'} as="style" />
<link
rel="stylesheet"
href={'style"][rel="stylesheet'}
precedence="style"
/>
<link rel="stylesheet" href={'with\\slashes'} precedence="style" />
foo
</div>,
);
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="preload" />
<link rel="stylesheet" href="style" data-rprec="style" />
<link rel="stylesheet" href="with\slashes" data-rprec="style" />
<link
rel="stylesheet"
href={'style"][rel="stylesheet'}
data-rprec="style"
/>
<link rel="preload" href={'preload"][rel="preload'} as="style" />
<link rel="preload" href={'style"][rel="stylesheet'} as="style" />
</head>
<body>
<div id="container">
<div>foo</div>
</div>
</body>
</html>,
);
});

// @gate enableFloat
it('escapes hrefs when selecting matching elements in the document when using preload and preinit', async () => {
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html>
<head />
<body>
<link rel="preload" href="preload" as="style" />
<link rel="stylesheet" href="style" precedence="style" />
<link rel="stylesheet" href="with\slashes" precedence="style" />
<div id="container" />
</body>
</html>,
);
pipe(writable);
});

function App() {
ReactDOM.preload('preload"][rel="preload', {as: 'style'});
ReactDOM.preinit('style"][rel="stylesheet', {
as: 'style',
precedence: 'style',
});
ReactDOM.preinit('with\\slashes', {
as: 'style',
precedence: 'style',
});
return <div>foo</div>;
}

container = document.getElementById('container');
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
expect(Scheduler).toFlushWithoutYielding();
expect(getVisibleChildren(document)).toEqual(
<html>
<head>
<link rel="preload" as="style" href="preload" />
<link rel="stylesheet" href="style" data-rprec="style" />
<link rel="stylesheet" href="with\slashes" data-rprec="style" />
<link
rel="stylesheet"
href={'style"][rel="stylesheet'}
data-rprec="style"
/>
<link rel="preload" href={'preload"][rel="preload'} as="style" />
</head>
<body>
<div id="container">
<div>foo</div>
</div>
</body>
</html>,
);
});
});
});

0 comments on commit f1925a8

Please sign in to comment.