Skip to content

Commit

Permalink
feat: improved origin isolation check (#148)
Browse files Browse the repository at this point in the history
* feat: improved origin isolation check

This simplifies online, cors and origin checks + adds a security
disclaimer regarding gateways that do not provide Origin isolation.

License: MIT
Signed-off-by: Marcin Rataj <lidel@lidel.org>
Co-authored-by: Peter Rabbitson <ribasushi@protocol.ai>
  • Loading branch information
lidel and ribasushi authored Jun 8, 2021
1 parent 55cdb43 commit abd4c1c
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 42 deletions.
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
}

0 comments on commit abd4c1c

Please sign in to comment.