Recently I’ve worked on the Firefox Bug 784402 – Pointer Lock must respect iframe sandbox flag.

This is a quick overview of what had to be done on the bug.

Sandbox flags

First lets check what the sandbox attribute does:
A quote from the w3c spec

The sandbox attribute, when specified, enables a set of extra restrictions on any content hosted by the iframe. Its value must be an unordered set of unique space-separated tokens that are ASCII case-insensitive. The allowed values are allow-forms, allow-popups, allow-same-origin, allow-scripts, and allow-top-navigation. When the attribute is set, the content is treated as being from a unique origin, forms and scripts are disabled, links are prevented from targeting other browsing contexts, and plugins are secured. The allow-same-origin keyword allows the content to be treated as being from the same origin instead of forcing it into a unique origin, the allow-top-navigation keyword allows the content to navigate its top-level browsing context, and the allow-forms, allow-popups and allow-scripts keywords re-enable forms, popups, and scripts respectively.

With pointerlock landing on Firefox 15, it was decided that a new sandbox flag should be created to restrict the pointerlock usage on embedded scripts in a page, so for example: if you add an advertisement script on your page, you don’t want to give the permissions to the advertisement to lock the pointer to itself.
To manage that, the allow-pointer-lock sandbox was created.

An overview of how the sandbox flags work:
List of flags:

 
 /**  
 * This flag prevents content from navigating browsing contexts other than  
 * the sandboxed browsing context itself (or browsing contexts further  
 * nested inside it), and the top-level browsing context.  
 */  
 const unsigned long SANDBOXED_NAVIGATION = 0x1;

/**  
 * This flag prevents content from navigating their top-level browsing  
 * context.  
 */  
 const unsigned long SANDBOXED_TOPLEVEL_NAVIGATION = 0x2;

/**  
 * This flag prevents content from instantiating plugins, whether using the  
 * embed element, the object element, the applet element, or through  
 * navigation of a nested browsing context, unless those plugins can be  
 * secured.  
 */  
 const unsigned long SANDBOXED_PLUGINS = 0x4;

/**  
 * This flag forces content into a unique origin, thus preventing it from  
 * accessing other content from the same origin.  
 * This flag also prevents script from reading from or writing to the  
 * document.cookie IDL attribute, and blocks access to localStorage.  
 */  
 const unsigned long SANDBOXED_ORIGIN = 0x8;

/**  
 * This flag blocks form submission.  
 */  
 const unsigned long SANDBOXED_FORMS = 0x10;

/**  
 * This flag blocks script execution.  
 */  
 const unsigned long SANDBOXED_SCRIPTS = 0x20;

/**  
 * This flag blocks features that trigger automatically, such as  
 * automatically playing a video or automatically focusing a form control.  
 */  
 const unsigned long SANDBOXED_AUTOMATIC_FEATURES = 0x40;

/**  
 * This flag blocks the document from acquiring pointerlock.  
 */  
 const unsigned long SANDBOXED_POINTER_LOCK = 0x80;  

Parsing the flags

So we have a 32 bit integer to store the sandbox flags.

Breaking down the integer we have 8 bytes
We can represent each byte in hexadecimal format:

So the number 0xFFFFFFFF has all the bits turned ON

Knowing that, we could use each bit of the integer to represent a flag.
We don’t care about the decimal value of that integer, since we are using it to store flags and not values.
So by saying 0x1, we are telling to turn the first bit of the first byte on, 0x2 turns the second bit of the first byte on
0x10 on the other hand tells to turn the first bit of the second byte on.
Remember that we are using hexadecimal notation.

So in the end, what’s happening is that each flag is turning a different bit on the integer

Later we’ll be able to check if that specific bit is ON or OFF and determine the status of the flag.

One thing to keep in mind is that if the iframe doesn’t have the sandbox attribute, then all the flags are turned OFF by default.

 
 <iframe></iframe>  

If the iframe has an empty sandbox attribute, then all the flags are ON by default

 
 <iframe sandbox=""></iframe>  

To turn the flags off, you can specify the feature you want to enable in the sandbox attribute:

   
 <iframe sandbox="allow-pointer-lock allow-same-origin"></iframe>  

In the snippet above both the allow-pointer-lock and allow-same-origin flag would be turned OFF, all the other flags would be ON

This is the code that parses the sandbox flags:

   
 /**  
 * A helper function that parses a sandbox attribute (of an <iframe> or
 * a CSP directive) and converts it to the set of flags used internally.
 *
 * @param aAttribute the value of the sandbox attribute
 * @return the set of flags
 */
uint32_t
nsContentUtils::ParseSandboxAttributeToFlags(const nsAString & aSandboxAttrValue) {
  // If there’s a sandbox attribute at all (and there is if this is being  
  // called), start off by setting all the restriction flags.  
  uint32_t out = SANDBOXED_NAVIGATION |
    SANDBOXED_TOPLEVEL_NAVIGATION |
    SANDBOXED_PLUGINS |
    SANDBOXED_ORIGIN |
    SANDBOXED_FORMS |
    SANDBOXED_SCRIPTS |
    SANDBOXED_AUTOMATIC_FEATURES |
    SANDBOXED_POINTER_LOCK;

  if (!aSandboxAttrValue.IsEmpty()) {
    // The separator optional flag is used because the HTML5 spec says any  
    // whitespace is ok as a separator, which is what this does.  
    HTMLSplitOnSpacesTokenizer tokenizer(aSandboxAttrValue, ‘‘,
      nsCharSeparatedTokenizerTemplate < nsContentUtils::IsHTMLWhitespace > ::SEPARATOR_OPTIONAL);

    while (tokenizer.hasMoreTokens()) {
      nsDependentSubstring token = tokenizer.nextToken();
      if (token.LowerCaseEqualsLiteral("allow-same-origin")) {
        out &= ~SANDBOXED_ORIGIN;
      } else if (token.LowerCaseEqualsLiteral("allow-forms")) {
        out &= ~SANDBOXED_FORMS;
      } else if (token.LowerCaseEqualsLiteral("allow-scripts")) {
        // allow-scripts removes both SANDBOXED_SCRIPTS and  
        // SANDBOXED_AUTOMATIC_FEATURES.  
        out &= ~SANDBOXED_SCRIPTS;
        out &= ~SANDBOXED_AUTOMATIC_FEATURES;
      } else if (token.LowerCaseEqualsLiteral("allow-top-navigation")) {
        out &= ~SANDBOXED_TOPLEVEL_NAVIGATION;
      } else if (token.LowerCaseEqualsLiteral("allow-pointer-lock")) {
        out &= ~SANDBOXED_POINTER_LOCK;
      }
    }
  }

  return out;
}

First all the flags are turned ON.
Then it checks if the sandbox attribute has any values, if it does it splits them and compares against the possible flags.
Once it finds a match, it does a BIT NEGATION on the flag and a BIT AND with the integer that has all the other flags.
What happens is that the flag being parsed is turned OFF.

In the end the integer with the status of all the flags is returned.

Locking the pointer

Now lets take a look at the code that checks for the allow-pointer-lock flag when an element requests pointerlock

 
 bool  
 nsDocument::ShouldLockPointer(Element* aElement)  
 {  
 // Check if pointer lock pref is enabled  
 if (!Preferences::GetBool("full-screen-api.pointer-lock.enabled")) {  
 NS_WARNING("ShouldLockPointer(): Pointer Lock pref not enabled");  
 return false;  
 }

 if (aElement != GetFullScreenElement()) {  
 NS_WARNING("ShouldLockPointer(): Element not in fullscreen");  
 return false;  
 }

 if (!aElement->IsInDoc()) {  
 NS_WARNING("ShouldLockPointer(): Element without Document");  
 return false;  
 }

 if (mSandboxFlags & SANDBOXED_POINTER_LOCK) {  
 NS_WARNING("ShouldLockPointer(): Document is sandboxed and doesn’t allow pointer-lock");  
 return false;  
 }

 // Check if the element is in a document with a docshell.  
 nsCOMPtr ownerDoc = aElement->OwnerDoc();  
 if (!ownerDoc) {  
 return false;  
 }  
 if (!nsCOMPtr(ownerDoc->GetContainer())) {  
 return false;  
 }  
 nsCOMPtr ownerWindow = ownerDoc->GetWindow();  
 if (!ownerWindow) {  
 return false;  
 }  
 nsCOMPtr ownerInnerWindow = ownerDoc->GetInnerWindow();  
 if (!ownerInnerWindow) {  
 return false;  
 }  
 if (ownerWindow->GetCurrentInnerWindow() != ownerInnerWindow) {  
 return false;  
 }

 return true;  
 }  

The ShouldLockPointer method is called every time an element requests pointerlock, the method does some sanity checks and makes sure everything is correct.
To check for the allow-pointer-lock sandbox flag, a BIT AND with the mSandBoxFlags and the SANDBOXPOINTERLOCK const is performed, we’ve looked at the SANDBOXPOINTERLOCK flag before, it has the value of 0x80
So if pointerlock is allowed, the mSandboxFlags would have the SANDBOXPOINTERLOCK flag OFF and the BIT AND would be false.

A big thanks to Ian Melven.
Ian is the one who implemented the sandbox attribute on Firefox and gave me some guidance on the PointerLock sandbox attribute bug.