Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<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 x
public
Bin info
anonymouspro
0viewers