<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js"></script>
<script src="https://rawgithub.com/nodeca/pako/1.0.6/dist/pako.js"></script>
</head>
<body>
<h1>Brand-sharing classes</h1>
<p>
This page demonstrates checking of a brand shared by a <em>family</em>
of ECMAScript classes (members of which could be defined in separate realms).
</p>
<p>
Brand checking is strong (but not perfectly protected) against false negatives,
but should be secure against false positives—even if built-in objects are altered
after class definition. In other words, no value should pass <code>isBrand</code>
unless it was constructed by a member of the class family with an unaltered
<code>proveBrand</code>.
</p>
<p>
The below editor allows you attack that claim.
Try to make a change that will trick an unaltered <code>Template1</code> or
<code>Template2</code> into accepting an altered <code>ownClass</code> value
as in-brand.
</p>
<section id="results">
<table>
<thead>
<tr><th><button type="button" id="test">Test</button></th></tr>
</thead>
<tbody>
<tr><th>.isOwnBrand( new Templated1() )</th></tr>
<tr><th>.isOwnBrand( new Templated2() )</th></tr>
<tr><th>.isOwnBrand( new Custom() )</th></tr>
</tbody>
<tbody>
<tr><th>.isBrand( new Templated1() )</th></tr>
<tr><th>.isBrand( new Templated2() )</th></tr>
<tr><th>.isBrand( new Custom() )</th></tr>
</tbody>
<tbody>
<tr><th>.isOwnBrand( new Proxy(new Templated1(), {}) )</th></tr>
<tr><th>.isBrand( new Proxy(new Templated1(), {}) )</th></tr>
</tbody>
</table>
</section>
<form id="controls">
<a id="link">shareable link</a>
<button type="button" id="reset">Reset</button>
<button type="button" id="default">Default</button>
</form>
<div id="attack"></div>
</body>
</html>
/* Flexbox FTW */
html, body {
margin: 0;
height: 100%;
}
body {
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 10px;
font-family: Tahoma, sans-serif;
}
body > * {
margin-top: 0;
}
section {
margin-bottom: 1em;
}
#attack {
min-height: 20em;
flex-grow: 1;
font-size: 100%;
}
#attack, #controls {
width: 95%;
max-width: 120ex;
margin-left: auto; margin-right: auto;
}
#link {
float: right;
}
#results .error {
color: red;
font-style: italic;
}
#results table {
border-collapse: collapse;
}
#results table th:not(:last-child) {
padding-right: 1em;
}
#results table tbody {
border-top: 1px solid black;
}
#results table tbody th {
text-align: left;
font-family: monospace;
}
#results .✔ {
color: darkgreen;
}
#results .✗ {
color: red;
}
#results.updating {
background-color: silver;
}
#results:not(.updating) {
transition: background-color 2s;
}
// Define two brand-sharing classes.
const classes = [];
for ( let _ of [0, 1] ) {
/*** BEGIN CLOSURE ***/
// Cache built-in values to protect against later manipulation.
const call = Function.prototype.call.bind(Function.prototype.call);
const fnToString = Function.prototype.call.bind(Function.prototype.toString);
const defineProperty = Object.defineProperty;
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const weakMapHas = WeakMap.prototype.has;
// privateData is a frozen WeakMap for unforgeable branding and truly private data.
const privateData = Object.freeze(
Object.defineProperties(
new WeakMap(),
Object.getOwnPropertyDescriptors(WeakMap.prototype)
)
);
// Templated is a brand-sharing class.
const ownClass =
class Templated {
// constructor brands the new instance and reserves space for its private data.
constructor() {
defineProperty(this, symBrand, { value: ownClass });
privateData.set(this, {});
}
// isOwnBrand checks if a value came from this constructor.
static isOwnBrand(obj) {
return privateData.has(obj);
}
// isBrand checks if a value came from any compatible constructor (e.g., cross-realm).
static isBrand(obj) {
return ownClass.proveBrand(obj) !== false;
}
// proveBrand verifies that a value came from any compatible constructor (e.g., cross-realm).
// It "shows its work" by using only built-in functions for access to internal slots
// and returning them so other compatible classes can verify their untampered nature.
static proveBrand(obj, onlyOwn = false) {
// First check for the value in our private WeakMap.
// We cannot avoid two property lookups for both using and sharing the `has` method,
// but we can take advantage of property writability and configurability invariants
// to prove that the second lookup uses the same value as the first.
// As an aside, this could be more clear if we had access to at least one of the following:
// * A static `WeakMap.has` with behavior like `Function.prototype.call.bind(WeakMap.prototype.has)`
// * A `Function.prototype.toString` that preserves the identity of bound built-in functions
// * A verifier of built-in values, e.g. `System.isBuiltIn(has.call, "Function.prototype.call")`
const privateDataHas = getOwnPropertyDescriptor(privateData, "has");
if ( privateData.has(obj) ) {
return { getOwnPropertyDescriptor, has: privateDataHas };
}
if ( onlyOwn ) {
return false;
}
// Check for a brand applied by a compatible constructor, and fail if there is none
// or if that constructor is ours (because our objects would all pass the previous check).
const brand = obj && obj[symBrand];
const prove = typeof brand === "function" && brand.proveBrand;
if ( typeof prove !== "function" || brand === ownClass || prove === ownProve ) {
return false;
}
// Verify brand compatibility with `Function.prototype.toString`
// (which has access to the [[ECMAScriptCode]] internal slot).
if ( fnToString(brand) !== fnToString(ownClass) || fnToString(prove) !== fnToString(ownProve) ) {
return false;
}
// Invoke the compatible proveBrand and assess its result
// by using built-in Function.prototype.toString
// to verify the identity of other built-in functions
// (regardless of realm).
const proof = prove(obj, true);
if ( !proof ) {
return false;
}
if ( fnToString(proof.getOwnPropertyDescriptor) === fnToString(getOwnPropertyDescriptor) &&
!proof.has.writable && !proof.has.configurable &&
fnToString(proof.has.value) === fnToString(weakMapHas) &&
call(proof.has.value, weakMap, ownClass) ) {
return proof;
}
return false;
}
};
// Lock down proveBrand.
defineProperty(ownClass, "proveBrand", { value: ownClass.proveBrand, enumerable: true });
// symBrand is a brand for the class family derived from its source text.
// This doesn't necessarily need to be a symbol,
// but it does need to compare equal across classes
// (so if it is a symbol, it must be in the cross-realm global registry
// as it is here).
const symBrand = Symbol.for(ownClass + "\uDEAD\uBEEF");
// Define closure values for use by the class.
const ownProve = ownClass.proveBrand;
const weakMap = new WeakMap([[ownClass, true]]);
/*** END CLOSURE ***/
// "export" the class.
classes.push(ownClass);
}
// Set up the demo functionality.
(function onLoad() {
// Bind the templated classes to identifiers.
const [ Templated1, Templated2 ] = classes;
// urlSafeEncode compresses a string and encodes the resulting octets to base64.
const urlSafeEncode = val => {
// return btoa(unescape(encodeURIComponent(String(val || ""))));
return btoa(String.fromCharCode(pako.deflate(String(val || ""))));
};
// urlSafeDecode inverts urlSafeEncode, decoding the base64 into octets
// and decompressing into the original string.
const urlSafeDecode = val => {
// return decodeURIComponent(escape(atob(String(val || ""))));
return val ?
new TextDecoder("utf-8").decode(pako.inflate(atob(String(val || "")))) :
"";
};
// Read in source text from the page URL (if any) and the active <script> element.
const requestClosureSrc = urlSafeDecode( (location.hash.match(/src=(.*)/) || [])[1] );
const scriptSrc = [document.querySelectorAll("script")].pop().innerText;
const closureSrc = scriptSrc.replace(/[^]*BEGIN[ ]CLOSURE.*[\r\n]*|[\r\n]*.*END[ ]CLOSURE[^]*/g, "")
.split(/\r?\n/g)
.map((line, i, lines) => {
if ( i === 0 ) {
const indent = line.match(/^\s+/g);
lines.indent = indent ? new RegExp("^" + indent) : /$^/;
lines.reduceIndent = true;
}
if ( line !== "" ) {
lines.reduceIndent = lines.reduceIndent ?
(line !== (line = line.replace(lines.indent, ""))) :
line.match(lines.indent) === null;
}
return line + "\n";
})
.join("");
const classSrc = classes[0].toString();
// Configure and initialize the editor.
const editor = ace.edit("attack");
editor.session.setMode("ace/mode/javascript");
editor.setTheme("ace/theme/tomorrow_night");
editor.session.on('change', delta => {
// { start, end, lines, action } = delta
document.getElementById("link").href = "#src=" + urlSafeEncode(editor.getValue());
});
document.getElementById("reset").addEventListener("click", evt => {
editor.session.setValue(requestClosureSrc || closureSrc);
});
document.getElementById("default").addEventListener("click", evt => {
editor.session.setValue(closureSrc);
});
document.getElementById("reset").click();
// Test submitted attacks upon request.
document.getElementById("test").addEventListener("click", evt => {
const results = document.getElementById("results");
results.classList.add("updating");
// Remove old results.
for ( let el of results.querySelectorAll(".error, tr > :nth-child(n+2)") ) {
el.parentNode.removeChild(el);
}
try {
// Evaluate the user class.
const src = editor.getValue();
const Custom = eval(src + "; ownClass");
for ( let classLabel of ["Templated1", "Templated2", "Custom"] ) {
for ( let row of results.querySelectorAll("tr") ) {
// Add a column header for each class.
if ( /thead/i.test(row.parentNode.nodeName) ) {
let heading = `<code>${classLabel}</code>`;
if ( classLabel === "Custom" ) {
if ( src === closureSrc ) {
heading += " <small>(no changes)</small>";
} else if ( src.replace(/\s+/g, "") === closureSrc.replace(/\s+/g, "") ) {
heading += " <small>(whitespace changes)</small>";
}
}
row.appendChild(document.createElement("th")).innerHTML = heading;
continue;
}
// Perform checks against each class and report the results.
const report = row.appendChild(document.createElement("td"));
try {
let result = eval(classLabel + row.firstChild.innerText);
if ( result === true ) {
report.className = result = "✔";
} else if ( result === false ) {
report.className = result = "✗";
}
report.innerText = result;
} catch ( ex ) {
report.classList.add("error");
report.innerText = ex.message;
}
}
}
} catch ( ex ) {
const error = results.appendChild(document.createElement("span"));
error.classList.add("error");
error.innerText = ex.message;
}
results.classList.remove("updating");
});
document.getElementById("test").click();
})();
Output
This bin was created anonymously and its free preview time has expired (learn why). — Get a free unrestricted account
Dismiss xKeyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |