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

feat: improved origin isolation check #148

Merged
merged 2 commits into from
Jun 8, 2021
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Then, submit a pull request for this change. Be sure to follow all the direction
## Testing locally

```console
$ npx http-server . -a 127.0.0.1 -p 3000 -c-1
$ npx serve -l 3000
```

## Command line
Expand Down
102 changes: 68 additions & 34 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

const HASH_TO_TEST = 'bafybeifx7yeb55armcsxwwitkymga5xf53dxiarykms3ygqic223w5sk3m';
const SCRIPT_HASH = 'bafybeietzsezxbgeeyrmwicylb5tpvf7yutrm3bxrfaoulaituhbi7q6yi';
const IMG_HASH = 'bafybeibwzifw52ttrkqlikfzext5akxu7lz4xiwjgwzmqcpdzmp3n5vnbe'; // 1x1.png
// const IFRAME_HASH = 'bafkreifx3g6bkkwl7b4v43lvcqfo5vshbiehuvmpky2zayhfpg5qj7y3ca'
const HASH_STRING = 'Hello from IPFS Gateway Checker';

const ipfs_http_client = window.IpfsHttpClient({
Expand Down Expand Up @@ -100,47 +100,79 @@ function checkViaImgSrc (imgUrl) {
reject()
}
img.onload = () => {
// subdomain works
timeout()
resolve()
}
// now - ensures we don't read from browser cache
// filename - ensures correct content-type is returned / sniffed
// x-ipfs-companion-no-redirect - hint for our browser extension, makes sure we test remote server
img.src = `${imgUrl}?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`
img.src = imgUrl
})
}

// This tries to load iframe and talk to it over postMessage
// (tests CORS isolation)
/* TODO: decide if this is useful
function checkViaIframe (gateway) {
const gwUrl = new URL(gateway)
// now - ensures we don't read from browser cache
// filename - ensures correct content-type is returned / sniffed
// x-ipfs-companion-no-redirect - hint for our browser extension, makes sure we test remote server
const now = Date.now()
const iframePathUrl = new URL(`${gwUrl.protocol}//${gwUrl.hostname}/ipfs/${IFRAME_HASH}?now=${now}&filename=origin-check.html#x-ipfs-companion-no-redirect`)
const iframeSubdomainUrl = new URL(`${gwUrl.protocol}//${IFRAME_HASH}.ipfs.${gwUrl.hostname}/?now=${now}&filename=origin-check.html#x-ipfs-companion-no-redirect`)
const iframeCheckTimeout = 15000
return new Promise((resolve, reject) => {
const timeout = () => {
if (!timer) return false
clearTimeout(timer)
timer = null
return true
}
let timer = setTimeout(() => { if (timeout()) reject() }, iframeCheckTimeout)
const iframe = document.createElement("iframe")
iframe.src = iframePathUrl.toString()
iframe.name = `iframe-${gwUrl.hostname}`
iframe.style.display = 'none'
document.body.appendChild(iframe)
iframe.onerror = () => {
timeout()
reject()
}
iframe.onload = () => {
window.addEventListener("message", (event) => {
if (event.origin === iframeSubdomainUrl.origin) {
console.log('checkViaIframe.event', event)
timeout()
resolve()
}
}, false)
iframe.contentWindow.postMessage("hello there! is your origin correct?", iframeSubdomainUrl.origin)
}
})
}
*/

Status.prototype.check = function() {
let gatewayAndScriptHash = this.parent.gateway.replace(":hash", SCRIPT_HASH);

// we set a unused number as a url parameter, to try to prevent content caching
// is it right ? ... do you know a better way ? ... does it always work ?
let now = Date.now();

// 3 important things here
// 1) we add #x-ipfs-companion-no-redirect to the final url (self explanatory)
// 2) we add ?filename=anyname.js as a parameter to let the gateway guess Content-Type header
// to be sent in headers in order to prevent CORB
// 3) parameter 'i' is the one used to identify the gateway once the script executes
let src = `${gatewayAndScriptHash}?i=${this.parent.index}&now=${now}&filename=anyname.js#x-ipfs-companion-no-redirect`;

let script = document.createElement('script');
script.src = src;
document.body.append(script);
script.onerror = () => {
// test by loading subresource via img.src (path will work on both old and subdomain gws)
const gwUrl = new URL(this.parent.gateway)
const imgPathUrl = new URL(`${gwUrl.protocol}//${gwUrl.hostname}/ipfs/${IMG_HASH}?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`)
checkViaImgSrc(imgPathUrl).then((res) => {
this.tag.textContent = '✅';
this.parent.checked()
}).catch(() => {
// we check this because the gateway could be already checked by CORS before onerror executes
// and, even though it is failing here, we know it is UP
if (!this.up) {
this.up = false;
this.tag.textContent = '❌';
this.parent.failed();
}
};
})
};

Status.prototype.checked = function() {
this.up = true;
this.tag.innerHTML = '✅';
this.parent.tag.classList.add('online')
};

// this function is executed from that previously loaded script
Expand All @@ -166,14 +198,16 @@ let Cors = function(parent) {
};

Cors.prototype.check = function() {
const gatewayAndHash = this.parent.gateway.replace(':hash', HASH_TO_TEST)
const now = Date.now()
const gatewayAndHash = this.parent.gateway.replace(':hash', HASH_TO_TEST)
const testUrl = `${gatewayAndHash}?now=${now}#x-ipfs-companion-no-redirect`
// response body can be accessed only if fetch was executed when
// liberal CORS is present (eg. '*')
fetch(testUrl).then((res) => res.text()).then((text) => {
const matched = (HASH_STRING === text.trim())
if (matched) {
this.parent.checked()
this.tag.textContent = ''
this.tag.textContent = '*'
this.parent.tag.classList.add('cors')
} else {
this.onerror()
Expand All @@ -182,7 +216,7 @@ Cors.prototype.check = function() {
}

Cors.prototype.onerror = function() {
this.tag.textContent = '';
this.tag.textContent = '';
};

let Origin = function(parent) {
Expand All @@ -193,21 +227,21 @@ let Origin = function(parent) {
};

Origin.prototype.check = function() {
// we are unable to check url after subdomain redirect because some gateways
// may not have proper CORS in place. instead, we manually construct subdomain
// URL and check if it loading known image works
const imgUrl = new URL(this.parent.gateway)
imgUrl.pathname = '/'
imgUrl.hostname = `${IMG_HASH}.ipfs.${imgUrl.hostname}`
checkViaImgSrc(imgUrl.toString()).then((res) => {
// we are unable to check url after subdomain redirect because some gateways
// may not have proper CORS in place. instead, we manually construct subdomain
// URL and check if it loading known image works
const gwUrl = new URL(this.parent.gateway)
// const imgPathUrl = new URL(`${gwUrl.protocol}//${gwUrl.hostname}/ipfs/${IMG_HASH}?now=${now}&filename=1x1.png#x-ipfs-companion-no-redirect`)
const imgSubdomainUrl = new URL(`${gwUrl.protocol}//${IMG_HASH}.ipfs.${gwUrl.hostname}/?now=${Date.now()}&filename=1x1.png#x-ipfs-companion-no-redirect`)
checkViaImgSrc(imgSubdomainUrl).then((res) => {
this.tag.textContent = '✅';
this.parent.tag.classList.add('origin')
this.parent.checked()
}).catch(() => this.onerror())
}

Origin.prototype.onerror = function() {
this.tag.textContent = '';
this.tag.textContent = '⚠️';
};

let Flag = function(parent, hostname) {
Expand Down
1 change: 1 addition & 0 deletions gateways.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"https://dweb.link/ipfs/:hash",
"https://gateway.ipfs.io/ipfs/:hash",
"https://ipfs.infura.io/ipfs/:hash",
"https://infura-ipfs.io/ipfs/:hash",
"https://ninetailed.ninja/ipfs/:hash",
"https://ipfs.globalupload.io/:hash",
"https://10.via0.com/ipfs/:hash",
Expand Down
16 changes: 10 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
<script src="https://cdn.jsdelivr.net/npm/ipfs-http-client@44.1.1/dist/index.min.js" integrity="sha256-TMFHdG0nkNPPHBpd2UNil3TMjSPxWLcRgvwANAmjuRg=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/ipfs-geoip@6.0.0/dist/index.min.js" integrity="sha256-Vhr0hZsdmsT81gd4u2bs3bLYLfDdr46nvcI/VaT9YZ4=" crossorigin="anonymous"></script>
<link href="styles.css" type="text/css" rel="stylesheet"/>
<link href="styles.css?v=0.4" type="text/css" rel="stylesheet"/>
</head>
<body id="checker" class="sans-serif charcoal bg-snow-muted">

Expand All @@ -35,17 +35,21 @@ <h1 class='f3 fw2 montserrat aqua ttu ma0'>Public Gateways</h1>
</header>

<div class="ph4-l pt4-l">
<div id="origin-warning" class="f5 pb2">
<strong>Security disclaimer:</strong> avoid storing sensitive data (or providing credentials) on websites loaded via gateways marked with <big>⚠️ </big><br/>
These are legacy gateways for fetching standalone data, not designed to serve dapps/websites (<strong>they do not provide <a href="https://docs.ipfs.io/how-to/address-ipfs-on-web/#path-gateway">origin isolation</a></strong>).
</div>
<div id="checker.stats" class="Stats monospace f6"></div>
<div id="checker.results" class="Results monospace f6">
<div class="Node">
<div class="Status truncate" title="Online status" style="cursor: help">Online</div>
<div class="Cors truncate" title="Allows Cross-Origin Resource Sharing"><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers" target="_blank" style="cursor: help">CORS</a></div>
<div class="Origin truncate" title="Provides Orign Isolation"><a href="https://docs.ipfs.io/guides/guides/addressing/#subdomain-gateway" target="_blank" style="cursor: help">Origin</a></div>
<div class="Link truncate">Hostname</div>
<div class="Status truncate" title="Online status: is it possible to read data?" style="cursor: help">Online</div>
<div class="Cors truncate" title="Allows Cross-Origin Resource Sharing (CORS fetch)"><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers" target="_blank">CORS</a></div>
<div class="Origin truncate" title="Provides Orign Isolation (Subdomain Gateway)"><a href="https://docs.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway" target="_blank">Origin</a></div>
<div class="Link truncate">Hostname</div>
<div class="Took">ΔT</div>
</div>
</div>
</div>
<script src="./app.js?v=0.4"></script>
</body>
<script src="./app.js?v=0.3"></script>
</html>
5 changes: 4 additions & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ div.Node div.Link {
text-overflow: ellipsis;
}

div.Node a {
div.Node a, div#origin-warning a {
text-decoration: none;
color: #357edd;
white-space: nowrap;
Expand Down Expand Up @@ -94,3 +94,6 @@ div.Node div.Took {
div.Node.origin div.Link::after {
content: " 💚"
}
div.Node:not(.online):not(:first-child) {
opacity: .5
}