
























If Content Security Policy is enabled for protection against cross-site scripting attacks (i.e. the unsafe-inline option is not set), the use of inline <script>s is not allowed. In that case, how can we pass server-generated data to the front-end without negatively affecting load time and run-time performance?
A common way to pass server-generated JSON-formatted data to the client so that it can be used in JavaScript is the following:
<!-- at the bottom of the HTML document, before `</body>` -->
<script>
window.data = <?php
// Note: this is potentially unsafe; see escaping instructions below.
echo json_encode($data);
?>;
</script>
<script src="process-data.js"></script>
This results in something like:
<!-- at the bottom of the HTML document, before `</body>` -->
<script>
window.data = {"foo":"bar&baz","baz":42,"qux":"lorem isn't ipsum","waldo":"2<5"};
</script>
<script src="process-data.js"></script>
Note that in this case, any occurrences of </script in the JSON-formatted data should be escaped as <\/script to avoid closing the <script> element prematurely. Also, <!-- must be escaped as \u003C!--. Other than that, no special escaping is necessary. In PHP, this boils down to json_encode($data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES);.
The file process-data.js looks something like this:
(function() {
var process = function(data) {
// Do stuff with the data, e.g. render charts, modify the DOM, whatever.
};
process(window.data);
}());
This technique is simple to implement and doesn’t have a negative impact on the page’s performance. Only a single HTTP request is needed to load the external JavaScript file that processes the data. The data itself is inlined in the HTML so no additional HTTP requests are needed to download it. Moreover, it gets parsed as a JavaScript object right away by the browser’s JavaScript engine — there’s no need to call JSON.parse() at run-time.
However, if Content Security Policy is enabled for protection against cross-site scripting attacks (i.e. the unsafe-inline option is not set), the use of inline scripts is not allowed. In that case, how can we still pass the data along without negatively affecting load time and run-time performance?
A naïve solution would be to load the JSON-formatted data through Ajax on page load. The HTML code then looks like this:
<script src="fetch-and-process-data.js"></script>
The file fetch-and-process-data.js looks something like this:
(function() {
var getJSON = function(url, callback) {
// Load the data using XHR, and call `callback(data)` when finished.
// See https://mathiasbynens.be/notes/xhr-responsetype-json for an example implementation.
};
var process = function(data) {
// Do stuff with the data, e.g. render charts, modify the DOM, whatever.
};
// Load the data, then process it.
getJSON('data.json', process);
}());
However, this introduces another HTTP request to load the JSON-formatted data, which negatively impacts load-time performance. This leads to a slower user experience, especially in cases where the HTML document doesn’t really contain any information until the data is loaded (remember Twitter back in 2010?).
What are the alternatives?
We can still inline the JSON-formatted data in the HTML document without using <script> elements (to satisfy CSP) by using a hidden dummy element. Note that the <data> element shouldn’t be used for this purpose, as in this case no human-readable representation of the data is available. Let’s use a <div> element instead:
<div id="important-data" class="hidden-data">
{"foo":"bar&baz","baz":42,"qux":"lorem isn't ipsum","waldo":"2<5"}
</div>
<script src="read-and-process-data.js"></script>
Note that in this case, the JSON-formatted data needs some extra escaping for security reasons:
< should be escaped as < (at the HTML level) or \u003C (at the JSON level) to avoid accidentally creating unwanted HTML elements and to avoid closing the <div> element prematurely.& should be escaped as & (at the HTML level) or \u0026 (at the JSON level) to avoid issues with ambiguous ampersands or text that looks like HTML entities in the source data.No other special escaping is necessary. In PHP, this boils down to json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES);.
To prevent browsers from displaying the raw data, CSS can be used:
.hidden-data {
display: none;
}
(Note that using the hidden HTML attribute instead wouldn’t be appropriate, since the raw data is never to be displayed.)
The file read-and-process-data.js looks something like this:
(function() {
var getData = function() {
// Read the JSON-formatted data from the DOM.
var element = document.getElementById('important-data');
var string = element.textContent || element.innerText; // fallback for IE ≤ 8
var data = JSON.parse(string);
// Clear the element’s contents now that we have a copy of the data.
element.innerHTML = '';
return data;
};
var process = function(data) {
// Do stuff with the data, e.g. render charts, modify the DOM, whatever.
};
// Get the data, then process it.
var data = getData();
process(data);
}());
This approach can be used even when CSP is enabled. That’s a big plus! However, it comes with a few downsides compared to the inline <script>-based technique:
JSON.parse() must be called at runtime. For backwards compatibility with older browsers, a JSON polyfill is needed.<script> element with a non-JavaScript typeTo get rid of the CSS dependency, we could use an element that browsers hide by default instead of a <div>, such as an inline <script> element with a non-JavaScript type attribute (so CSP doesn’t block it or trigger any warnings):
<script type="application/json" id="important-data">
{"foo":"bar&baz","baz":42,"qux":"lorem isn't ipsum","waldo":"2<5"}
</script>
<script src="read-and-process-data.js"></script>
Note that in this case, any occurrences of </script in the JSON-formatted data should be escaped as <\/script to avoid closing the <script> element prematurely. Since script elements with unknown types are not executed by any browser, there should be no risk in leaving <! unescaped — but feel free to escape it as \u003C! just in case. Other than that, no special escaping is necessary. In PHP, this boils down to json_encode($data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES);.
The read-and-process-data.js script is exactly the same as in the previous example.
data-* attributeAnother solution is to use a custom data-* attribute to hide the JSON-formatted data in the DOM. You could assign this attribute to the <html> or <body> element, but I’d suggest injecting the data before any <script> elements near the closing </body> tag, so that the rest of the content is downloaded and rendered first.
<div id="important-data" class="hidden-data" data-data='{"foo":"bar&baz","baz":42,"qux":"lorem isn't ipsum","waldo":"2<5"}'></div>
<script src="read-and-process-data.js"></script>
Note that I’ve wrapped the attribute value in single quotes instead of double quotes to avoid having to escape the many double quotes in the JSON-formatted data. Still, the JSON-formatted data needs some extra escaping for security reasons:
' should be escaped as ' or ' (at the HTML level) or as \u0027 (at the JSON level) to avoid breaking out of the HTML attribute value. (If the attribute value is wrapped in double quotes, escape " as " or \u0022 instead.)& should be escaped as & (at the HTML level) or \u0026 (at the JSON level) to avoid issues with ambiguous ampersands or text that looks like HTML entities in the source data.No other special escaping is necessary. In PHP, this boils down to json_encode($data, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES);. Note that < and > don’t need escaping in quoted attribute values.
The file read-and-process-data.js looks something like this:
(function() {
var getData = function() {
// Read the JSON-formatted data from the DOM.
var element = document.getElementById('important-data');
// Note: in modern browsers, you could use `element.dataset.data` instead
// of `getAttribute('data-data')`.
var string = element.getAttribute('data-data');
var data = JSON.parse(string);
// Remove the attribute now that we have a copy of the data.
element.removeAttribute('data-data');
return data;
};
var process = function(data) {
// Do stuff with the data, e.g. render charts, modify the DOM, whatever.
};
// Get the data, then process it.
var data = getData();
process(data);
}());
Just like the “hidden element” approach, this technique can also be used even when CSP is enabled. Another advantage: since the data is part of an attribute value, it won’t be visible by default.
However, it still has a few downsides compared to the inline <script>-based technique:
JSON.parse() must be called at runtime. For backwards compatibility with older browsers, a JSON polyfill is needed.Passing server-generated JSON-formatted data to the client-side for use in JavaScript becomes a bit more complex when CSP is enabled, since inline <script>s are not an option in that case.
With security, performance, and flexibility in mind, the best solution is to hide the JSON-formatted data either in a custom data-* attribute on an element near the closing </body> tag, or in an inline <script> element with type="application/json".
Disclaimer: Enabling CSP is not enough to fully protect your site against client-side security vulnerabilities, although it certainly helps.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。