Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime checks referrer policy #372

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"features": {
"runtimeChecks": {
"state": "enabled",
"exceptions": [],
"settings": {
"taintCheck": "enabled",
"matchAllDomains": "enabled",
"matchAllStackDomains": "enabled",
"overloadInstanceOf": "enabled",
"injectGlobalStyles": "disabled",
"shadowDom": "enabled",
"domains": [
],
"stackDomains": [
],
"tagModifiers": {
"script": {
"referrerPolicy": "origin"
Copy link
Contributor Author

@jonathanKingston jonathanKingston Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably be:

"script": {
    "treatments": {
        "referrerPolicy": {
            "value": "origin"
        }
    }
}

Where "treatments" could be policies, schemes etc.

}
}
}
}
}
}
1 change: 1 addition & 0 deletions integration-test/test-pages/runtime-checks/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<li><a href="./pages/filter-props.html">Filter props</a> - <a href="./config/filter-props.json">Config</a></li>
<li><a href="./pages/script-overload.html">Script overloading</a> - <a href="./config/script-overload.json">Config</a></li>
<li><a href="./pages/shadow-dom.html">Shadow dom support</a> - <a href="./config/shadow-dom.json">Config</a></li>
<li><a href="./pages/referrer-stripping.html">Referrer stripping support</a> - <a href="./config/referrer-stripping.json">Config</a></li>
</ul>

</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Runtime checks</title>
<link rel="stylesheet" href="../../shared/style.css">
</head>
<body>
<script src="../../shared/utils.js"></script>
<p><a href="../index.html">[Runtime checks]</a></p>

<p>This page verifies referrer can be stripped <a href="../config/basic-run.json">config</a></p>

<script>
test('Script should have referrer policy set', async () => {
window.scripty1Ran = false;
const scriptElement = document.createElement('script');
scriptElement.innerText = 'window.scripty1Ran = true';
scriptElement.id = 'scripty';
scriptElement.setAttribute('type', 'application/javascript');
document.body.appendChild(scriptElement);
const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks:last-of-type');
const instanceofResult = scriptElement instanceof HTMLScriptElement;
const scripty = document.querySelector('#scripty');

return [
{ name: 'hadInspectorNode', result: hadInspectorNode, expected: true },
{ name: 'expect script to match', result: scripty, expected: scriptElement },
{ name: 'instanceof matches HTMLScriptElement', result: instanceofResult, expected: true },
{ name: 'scripty.type', result: scripty.type, expected: 'application/javascript' },
{ name: 'scripty.id', result: scripty.id, expected: 'scripty' },
{ name: 'scripty.referrerPolicy', result: scripty.referrerPolicy, expected: 'origin' },
{ name: 'script ran', result: window.scripty1Ran, expected: true },
{ name: 'script ran', result: scriptElement.innerText, expected: 'window.scripty1Ran = true' },
];
});

test('Script ensure no downgrade', async () => {
window.scripty1Ran = false;
const scriptElement = document.createElement('script');
scriptElement.innerText = 'window.scripty1Ran = true';
scriptElement.id = 'scripty';
scriptElement.referrerPolicy = 'no-referrer';
scriptElement.setAttribute('type', 'application/javascript');
document.body.appendChild(scriptElement);
const hadInspectorNode = scriptElement === document.querySelector('ddg-runtime-checks:last-of-type');
const instanceofResult = scriptElement instanceof HTMLScriptElement;
const scripty = document.querySelector('#scripty');

return [
{ name: 'hadInspectorNode', result: hadInspectorNode, expected: true },
{ name: 'expect script to match', result: scripty, expected: scriptElement },
{ name: 'instanceof matches HTMLScriptElement', result: instanceofResult, expected: true },
{ name: 'scripty.type', result: scripty.type, expected: 'application/javascript' },
{ name: 'scripty.id', result: scripty.id, expected: 'scripty' },
{ name: 'scripty.referrerPolicy', result: scripty.referrerPolicy, expected: 'no-referrer' },
{ name: 'script ran', result: window.scripty1Ran, expected: true },
{ name: 'script ran', result: scriptElement.innerText, expected: 'window.scripty1Ran = true' },
];
});

// eslint-disable-next-line no-undef
renderResults();
</script>
</body>
</html>
33 changes: 31 additions & 2 deletions src/features/runtime-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ let initialCreateElement
let tagModifiers = {}
let shadowDomEnabled = false
let scriptOverload = {}
const defaultInterrogatedTags = ['script']
let interrogatedTags = defaultInterrogatedTags
// Ignore monitoring properties that are only relevant once and already handled
const defaultIgnoreMonitorList = ['onerror', 'onload']
let ignoreMonitorList = defaultIgnoreMonitorList
Expand Down Expand Up @@ -195,6 +197,32 @@ class DDGRuntimeChecks extends HTMLElement {
el.textContent = '(function (parentScope) {' + constructProxy.toString() + ' ' + innerCode + '})(globalThis)'
}

handleReferrerPolicy (el) {
const supportedTags = {
script: 'src',
iframe: 'src',
img: 'src',
link: 'href',
a: 'href'
}
if (!supportedTags[this.#tagName]) return
const attr = supportedTags[this.#tagName]
const val = el[attr]
// Short circuit if we don't have a value
if (val === '' || val === null) {
return
}
// Short circuit if the referrer policy is already set to no-referrer
if (el.referrerPolicy === 'no-referrer') {
return
}

if (tagModifiers[this.#tagName]?.referrerPolicy) {
// Set the referrer policy to origin to prevent leaking the referrer to the script
el.referrerPolicy = tagModifiers[this.#tagName].referrerPolicy
}
}

/**
* The element has been moved to the DOM, so we can now reflect all changes to a real element.
* This is to allow us to interrogate the real element before it is moved to the DOM.
Expand Down Expand Up @@ -257,6 +285,7 @@ class DDGRuntimeChecks extends HTMLElement {
if (this.#tagName === 'script') {
this.computeScriptOverload(el)
}
this.handleReferrerPolicy(el)

// Move the new element to the DOM
try {
Expand Down Expand Up @@ -412,8 +441,7 @@ function overloadInstanceOfChecks (elementInterface) {
* @returns {boolean}
*/
function shouldInterrogate (tagName) {
const interestingTags = ['script']
if (!interestingTags.includes(tagName)) {
if (!interrogatedTags.includes(tagName)) {
return false
}
if (matchAllStackDomains) {
Expand Down Expand Up @@ -473,6 +501,7 @@ export default class RuntimeChecks extends ContentFeature {
shadowDomEnabled = this.getFeatureSettingEnabled('shadowDom') || false
scriptOverload = this.getFeatureSetting('scriptOverload') || {}
ignoreMonitorList = this.getFeatureSetting('ignoreMonitorList') || defaultIgnoreMonitorList
interrogatedTags = this.getFeatureSetting('interrogatedTags') || defaultInterrogatedTags

overrideCreateElement()

Expand Down