@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Emit a "Content-Security-Policy" HTTP header

Summary:
See PHI399. Ref T4340. This header provides an additional layer of protection against various attacks, including XSS attacks which embed inline `<script ...>` or `onhover="..."` content into the document.

**style-src**: The "unsafe-inline" directive affects both `style="..."` and `<style>`. We use a lot of `style="..."`, some very legitimately, so we can't realistically get away from this any time soon. We only use one `<style>` (for monospaced font preferences) but can't disable `<style>` without disabling `style="..."`.

**img-src**: We use "data:" URIs to inline small images into CSS, and there's a significant performance benefit from doing this. There doesn't seem to be a way to allow "data" URIs in CSS without allowing them in the document itself.

**script-src** and **frame-src**: For a small number of flows (Recaptcha, Stripe) we embed external javascript, some of which embeds child elements (or additional resources) into the document. We now whitelist these narrowly on the respective pages.

This won't work with Quicksand, so I've blacklisted it for now.

**connect-src**: We need to include `'self'` for AJAX to work, and any websocket URIs.

**Clickjacking**: We now have three layers of protection:

- X-Frame-Options: works in older browsers.
- `frame-ancestors 'none'`: does the same thing.
- Explicit framebust in JX.Stratcom after initialization: works in ancient IE.

We could probably drop the explicit framebust but it wasn't difficult to retain.

**script tags**: We previously used an inline `<script>` tag to start Javelin. I've moved this to `<data data-javelin-init ...>` tags, which seems to work properly.

**`__DEV__`**: We previously used an inline `<script>` tag to set the `__DEV__` mode flag. I tried using the "initialization" tags for this, but they fire too late. I moved it to `<html data-developer-mode="1">`, which seems OK everywhere.

**CSP Scope**: Only the CSP header on the original request appears to matter -- you can't refine the scope by emitting headers on CSS/JS. To reduce confusion, I disabled the headers on those response types. More headers could be disabled, although we're likely already deep in the land of diminishing returns.

**Initialization**: The initialization sequence has changed slightly. Previously, we waited for the <script> in bottom of the document to evaluate. Now, we go fishing for tags when domcontentready fires.

Test Plan:
- Browsed around in Firefox, Safari and Chrome looking for console warnings. Interacted with various Javascript behaviors. Enabled Quicksand.
- Disabled all the framebusting, launched a clickjacking attack, verified that each layer of protection is individually effective.
- Verified that the XHProf iframe in Darkconsole and the PHPAST frame layout work properly.
- Enabled notifications, verified no complaints about connecting to Aphlict.
- Hit `__DEV__` mode warnings based on the new data attribute.
- Tried to do sketchy stuff with `data:` URIs and SVGs. This works but doesn't seem to be able to do anything dangerous.
- Went through the Stripe and Recaptcha workflows.
- Dumped and examined the CSP headers with `curl`, etc.
- Added a raw <script> tag to a page (as though I'd found an XSS attack), verified it was no longer executed.

Maniphest Tasks: T4340

Differential Revision: https://secure.phabricator.com/D19143

+322 -70
+11 -11
resources/celerity/map.php
··· 10 10 'conpherence.pkg.css' => 'e68cf1fa', 11 11 'conpherence.pkg.js' => '15191c65', 12 12 'core.pkg.css' => '2fa91e14', 13 - 'core.pkg.js' => 'dc13d4b7', 13 + 'core.pkg.js' => '7aa5bd92', 14 14 'darkconsole.pkg.js' => '1f9a31bc', 15 15 'differential.pkg.css' => '113e692c', 16 16 'differential.pkg.js' => 'f6d809c0', ··· 211 211 'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2', 212 212 'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742', 213 213 'rsrc/externals/javelin/core/Event.js' => '2ee659ce', 214 - 'rsrc/externals/javelin/core/Stratcom.js' => '6ad39b6f', 214 + 'rsrc/externals/javelin/core/Stratcom.js' => '327f418a', 215 215 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4', 216 216 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 217 217 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', 218 218 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d', 219 - 'rsrc/externals/javelin/core/init.js' => '3010e992', 219 + 'rsrc/externals/javelin/core/init.js' => '638a4e2b', 220 220 'rsrc/externals/javelin/core/init_node.js' => 'c234aded', 221 221 'rsrc/externals/javelin/core/install.js' => '05270951', 222 222 'rsrc/externals/javelin/core/util.js' => '93cc50d6', ··· 722 722 'javelin-install' => '05270951', 723 723 'javelin-json' => '69adf288', 724 724 'javelin-leader' => '7f243deb', 725 - 'javelin-magical-init' => '3010e992', 725 + 'javelin-magical-init' => '638a4e2b', 726 726 'javelin-mask' => '8a41885b', 727 727 'javelin-quicksand' => '6b8ef10b', 728 728 'javelin-reactor' => '2b8de964', ··· 735 735 'javelin-router' => '29274e2b', 736 736 'javelin-scrollbar' => '9065f639', 737 737 'javelin-sound' => '949c0fe5', 738 - 'javelin-stratcom' => '6ad39b6f', 738 + 'javelin-stratcom' => '327f418a', 739 739 'javelin-tokenizer' => '8d3bc1b2', 740 740 'javelin-typeahead' => '70baed2f', 741 741 'javelin-typeahead-composite-source' => '503e17fd', ··· 1131 1131 'javelin-dom', 1132 1132 'javelin-workflow', 1133 1133 ), 1134 + '327f418a' => array( 1135 + 'javelin-install', 1136 + 'javelin-event', 1137 + 'javelin-util', 1138 + 'javelin-magical-init', 1139 + ), 1134 1140 '358b8c04' => array( 1135 1141 'javelin-install', 1136 1142 'javelin-util', ··· 1445 1451 ), 1446 1452 '69adf288' => array( 1447 1453 'javelin-install', 1448 - ), 1449 - '6ad39b6f' => array( 1450 - 'javelin-install', 1451 - 'javelin-event', 1452 - 'javelin-util', 1453 - 'javelin-magical-init', 1454 1454 ), 1455 1455 '6b8ef10b' => array( 1456 1456 'javelin-install',
+126 -1
src/aphront/response/AphrontResponse.php
··· 7 7 private $canCDN; 8 8 private $responseCode = 200; 9 9 private $lastModified = null; 10 - 10 + private $contentSecurityPolicyURIs; 11 + private $disableContentSecurityPolicy; 11 12 protected $frameable; 13 + 12 14 13 15 public function setRequest($request) { 14 16 $this->request = $request; ··· 17 19 18 20 public function getRequest() { 19 21 return $this->request; 22 + } 23 + 24 + final public function addContentSecurityPolicyURI($kind, $uri) { 25 + if ($this->contentSecurityPolicyURIs === null) { 26 + $this->contentSecurityPolicyURIs = array( 27 + 'script' => array(), 28 + 'connect' => array(), 29 + 'frame' => array(), 30 + ); 31 + } 32 + 33 + if (!isset($this->contentSecurityPolicyURIs[$kind])) { 34 + throw new Exception( 35 + pht( 36 + 'Unknown Content-Security-Policy URI kind "%s".', 37 + $kind)); 38 + } 39 + 40 + $this->contentSecurityPolicyURIs[$kind][] = (string)$uri; 41 + 42 + return $this; 43 + } 44 + 45 + final public function setDisableContentSecurityPolicy($disable) { 46 + $this->disableContentSecurityPolicy = $disable; 47 + return $this; 20 48 } 21 49 22 50 ··· 59 87 ); 60 88 } 61 89 90 + $csp = $this->newContentSecurityPolicyHeader(); 91 + if ($csp !== null) { 92 + $headers[] = array('Content-Security-Policy', $csp); 93 + } 94 + 62 95 return $headers; 96 + } 97 + 98 + private function newContentSecurityPolicyHeader() { 99 + if ($this->disableContentSecurityPolicy) { 100 + return null; 101 + } 102 + 103 + $csp = array(); 104 + 105 + $cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); 106 + if ($cdn) { 107 + $default = $this->newContentSecurityPolicySource($cdn); 108 + } else { 109 + $default = "'self'"; 110 + } 111 + 112 + $csp[] = "default-src {$default}"; 113 + 114 + // We use "data:" URIs to inline small images into CSS. This policy allows 115 + // "data:" URIs to be used anywhere, but there doesn't appear to be a way 116 + // to say that "data:" URIs are okay in CSS files but not in the document. 117 + $csp[] = "img-src {$default} data:"; 118 + 119 + // We use inline style="..." attributes in various places, many of which 120 + // are legitimate. We also currently use a <style> tag to implement the 121 + // "Monospaced Font Preference" setting. 122 + $csp[] = "style-src {$default} 'unsafe-inline'"; 123 + 124 + // On a small number of pages, including the Stripe workflow and the 125 + // ReCAPTCHA challenge, we embed external Javascript directly. 126 + $csp[] = $this->newContentSecurityPolicy('script', $default); 127 + 128 + // We need to specify that we can connect to ourself in order for AJAX 129 + // requests to work. 130 + $csp[] = $this->newContentSecurityPolicy('connect', "'self'"); 131 + 132 + // DarkConsole and PHPAST both use frames to render some content. 133 + $csp[] = $this->newContentSecurityPolicy('frame', "'self'"); 134 + 135 + // This is a more modern flavor of of "X-Frame-Options" and prevents 136 + // clickjacking attacks where the page is included in a tiny iframe and 137 + // the user is convinced to click a element on the page, which really 138 + // clicks a dangerous button hidden under a picture of a cat. 139 + if ($this->frameable) { 140 + $csp[] = "frame-ancestors 'self'"; 141 + } else { 142 + $csp[] = "frame-ancestors 'none'"; 143 + } 144 + 145 + $csp = implode('; ', $csp); 146 + 147 + return $csp; 148 + } 149 + 150 + private function newContentSecurityPolicy($type, $defaults) { 151 + if ($defaults === null) { 152 + $sources = array(); 153 + } else { 154 + $sources = (array)$defaults; 155 + } 156 + 157 + $uris = $this->contentSecurityPolicyURIs; 158 + if (isset($uris[$type])) { 159 + foreach ($uris[$type] as $uri) { 160 + $sources[] = $this->newContentSecurityPolicySource($uri); 161 + } 162 + } 163 + $sources = array_unique($sources); 164 + 165 + return "{$type}-src ".implode(' ', $sources); 166 + } 167 + 168 + private function newContentSecurityPolicySource($uri) { 169 + // Some CSP URIs are ultimately user controlled (like notification server 170 + // URIs and CDN URIs) so attempt to stop an attacker from injecting an 171 + // unsafe source (like 'unsafe-eval') into the CSP header. 172 + 173 + $uri = id(new PhutilURI($uri)) 174 + ->setPath(null) 175 + ->setFragment(null) 176 + ->setQueryParams(array()); 177 + 178 + $uri = (string)$uri; 179 + if (preg_match('/[ ;\']/', $uri)) { 180 + throw new Exception( 181 + pht( 182 + 'Attempting to emit a response with an unsafe source ("%s") in the '. 183 + 'Content-Security-Policy header.', 184 + $uri)); 185 + } 186 + 187 + return $uri; 63 188 } 64 189 65 190 public function setCacheDurationInSeconds($duration) {
+57 -26
src/applications/celerity/CelerityStaticResourceResponse.php
··· 17 17 private $behaviors = array(); 18 18 private $hasRendered = array(); 19 19 private $postprocessorKey; 20 + private $contentSecurityPolicyURIs = array(); 20 21 21 22 public function __construct() { 22 23 if (isset($_REQUEST['__metablock__'])) { ··· 35 36 $id = count($this->metadata); 36 37 $this->metadata[$id] = $metadata; 37 38 return $this->metadataBlock.'_'.$id; 39 + } 40 + 41 + public function addContentSecurityPolicyURI($kind, $uri) { 42 + $this->contentSecurityPolicyURIs[$kind][] = $uri; 43 + return $this; 44 + } 45 + 46 + public function getContentSecurityPolicyURIMap() { 47 + return $this->contentSecurityPolicyURIs; 38 48 } 39 49 40 50 public function getMetadataBlock() { ··· 196 206 $type)); 197 207 } 198 208 199 - public function renderHTMLFooter() { 209 + public function renderHTMLFooter($is_frameable) { 200 210 $this->metadataLocked = true; 201 211 202 - $data = array(); 203 - if ($this->metadata) { 204 - $json_metadata = AphrontResponse::encodeJSONForHTTPResponse( 205 - $this->metadata); 206 - $this->metadata = array(); 207 - } else { 208 - $json_metadata = '{}'; 209 - } 210 - // Even if there is no metadata on the page, Javelin uses the mergeData() 211 - // call to start dispatching the event queue. 212 - $data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '. 213 - $json_metadata.');'; 212 + $merge_data = array( 213 + 'block' => $this->metadataBlock, 214 + 'data' => $this->metadata, 215 + ); 216 + $this->metadata = array(); 214 217 215 - $onload = array(); 218 + $behavior_lists = array(); 216 219 if ($this->behaviors) { 217 220 $behaviors = $this->behaviors; 218 221 $this->behaviors = array(); ··· 241 244 if (!$group) { 242 245 continue; 243 246 } 244 - $group_json = AphrontResponse::encodeJSONForHTTPResponse( 245 - $group); 246 - $onload[] = 'JX.initBehaviors('.$group_json.')'; 247 + $behavior_lists[] = $group; 247 248 } 248 249 } 249 250 250 - if ($onload) { 251 - foreach ($onload as $func) { 252 - $data[] = 'JX.onload(function(){'.$func.'});'; 253 - } 251 + $initializers = array(); 252 + 253 + // Even if there is no metadata on the page, Javelin uses the mergeData() 254 + // call to start dispatching the event queue, so we always want to include 255 + // this initializer. 256 + $initializers[] = array( 257 + 'kind' => 'merge', 258 + 'data' => $merge_data, 259 + ); 260 + 261 + foreach ($behavior_lists as $behavior_list) { 262 + $initializers[] = array( 263 + 'kind' => 'behaviors', 264 + 'data' => $behavior_list, 265 + ); 254 266 } 255 267 256 - if ($data) { 257 - $data = implode("\n", $data); 258 - return self::renderInlineScript($data); 259 - } else { 260 - return ''; 268 + if ($is_frameable) { 269 + $initializers[] = array( 270 + 'data' => 'frameable', 271 + 'kind' => (bool)$is_frameable, 272 + ); 261 273 } 274 + 275 + $tags = array(); 276 + foreach ($initializers as $initializer) { 277 + $data = $initializer['data']; 278 + if (is_array($data)) { 279 + $json_data = AphrontResponse::encodeJSONForHTTPResponse($data); 280 + } else { 281 + $json_data = json_encode($data); 282 + } 283 + 284 + $tags[] = phutil_tag( 285 + 'data', 286 + array( 287 + 'data-javelin-init-kind' => $initializer['kind'], 288 + 'data-javelin-init-data' => $json_data, 289 + )); 290 + } 291 + 292 + return $tags; 262 293 } 263 294 264 295 public static function renderInlineScript($data) {
+5
src/applications/celerity/controller/CelerityResourceController.php
··· 106 106 $response = id(new AphrontFileResponse()) 107 107 ->setMimeType($type_map[$type]); 108 108 109 + // The "Content-Security-Policy" header has no effect on the actual 110 + // resources, only on the main request. Disable it on the resource 111 + // responses to limit confusion. 112 + $response->setDisableContentSecurityPolicy(true); 113 + 109 114 $range = AphrontRequest::getHTTPHeader('Range'); 110 115 111 116 if (strlen($range)) {
+7 -1
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 275 275 AphrontRequest $request, 276 276 array $errors) { 277 277 278 + $src = 'https://js.stripe.com/v2/'; 279 + 278 280 $ccform = id(new PhortuneCreditCardForm()) 279 281 ->setSecurityAssurance( 280 282 pht('Payments are processed securely by Stripe.')) 281 283 ->setUser($request->getUser()) 282 284 ->setErrors($errors) 283 - ->addScript('https://js.stripe.com/v2/'); 285 + ->addScript($src); 286 + 287 + CelerityAPI::getStaticResourceResponse() 288 + ->addContentSecurityPolicyURI('script', $src) 289 + ->addContentSecurityPolicyURI('frame', $src); 284 290 285 291 Javelin::initBehavior( 286 292 'stripe-payment-form',
+17 -9
src/view/form/control/AphrontFormRecaptchaControl.php
··· 42 42 $js = 'https://www.google.com/recaptcha/api.js'; 43 43 $pubkey = PhabricatorEnv::getEnvConfig('recaptcha.public-key'); 44 44 45 - return array( 46 - phutil_tag('div', array( 47 - 'class' => 'g-recaptcha', 48 - 'data-sitekey' => $pubkey, 49 - )), 45 + CelerityAPI::getStaticResourceResponse() 46 + ->addContentSecurityPolicyURI('script', $js) 47 + ->addContentSecurityPolicyURI('script', 'https://www.gstatic.com/') 48 + ->addContentSecurityPolicyURI('frame', 'https://www.google.com/'); 50 49 51 - phutil_tag('script', array( 52 - 'type' => 'text/javascript', 53 - 'src' => $js, 54 - )), 50 + return array( 51 + phutil_tag( 52 + 'div', 53 + array( 54 + 'class' => 'g-recaptcha', 55 + 'data-sitekey' => $pubkey, 56 + )), 57 + phutil_tag( 58 + 'script', 59 + array( 60 + 'type' => 'text/javascript', 61 + 'src' => $js, 62 + )), 55 63 ); 56 64 } 57 65 }
+8 -1
src/view/page/AphrontPageView.php
··· 59 59 ), 60 60 array($body, $tail)); 61 61 62 + if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { 63 + $data_fragment = phutil_safe_html(' data-developer-mode="1"'); 64 + } else { 65 + $data_fragment = null; 66 + } 67 + 62 68 $response = hsprintf( 63 69 '<!DOCTYPE html>'. 64 - '<html>'. 70 + '<html%s>'. 65 71 '<head>'. 66 72 '<meta charset="UTF-8" />'. 67 73 '<title>%s</title>'. ··· 69 75 '</head>'. 70 76 '%s'. 71 77 '</html>', 78 + $data_fragment, 72 79 $title, 73 80 $head, 74 81 $body);
+1 -9
src/view/page/PhabricatorBarePageView.php
··· 59 59 } 60 60 61 61 protected function getHead() { 62 - $framebust = null; 63 - if (!$this->getFrameable()) { 64 - $framebust = '(top == self) || top.location.replace(self.location.href);'; 65 - } 66 - 67 62 $viewport_tag = null; 68 63 if ($this->getDeviceReady()) { 69 64 $viewport_tag = phutil_tag( ··· 140 135 } 141 136 } 142 137 143 - $developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); 144 138 return hsprintf( 145 - '%s%s%s%s%s%s%s%s%s', 139 + '%s%s%s%s%s%s%s%s', 146 140 $viewport_tag, 147 141 $mask_icon, 148 142 $icon_tag_76, ··· 150 144 $icon_tag_152, 151 145 $favicon_tag, 152 146 $referrer_tag, 153 - CelerityStaticResourceResponse::renderInlineScript( 154 - $framebust.jsprintf('window.__DEV__=%d;', ($developer ? 1 : 0))), 155 147 $response->renderResourcesOfType('css')); 156 148 } 157 149
+26 -2
src/view/page/PhabricatorStandardPageView.php
··· 608 608 Javelin::initBehavior( 609 609 'aphlict-listen', 610 610 array( 611 - 'websocketURI' => (string)$client_uri, 611 + 'websocketURI' => (string)$client_uri, 612 612 ) + $this->buildAphlictListenConfigData()); 613 + 614 + CelerityAPI::getStaticResourceResponse() 615 + ->addContentSecurityPolicyURI('connect', $client_uri); 613 616 } 614 617 } 615 618 616 - $tail[] = $response->renderHTMLFooter(); 619 + $tail[] = $response->renderHTMLFooter($this->getFrameable()); 617 620 618 621 return $tail; 619 622 } ··· 860 863 $blacklist[] = $application->getQuicksandURIPatternBlacklist(); 861 864 } 862 865 866 + // See T4340. Currently, Phortune and Auth both require pulling in external 867 + // Javascript (for Stripe card management and Recaptcha, respectively). 868 + // This can put us in a position where the user loads a page with a 869 + // restrictive Content-Security-Policy, then uses Quicksand to navigate to 870 + // a page which needs to load external scripts. For now, just blacklist 871 + // these entire applications since we aren't giving up anything 872 + // significant by doing so. 873 + 874 + $blacklist[] = array( 875 + '/phortune/.*', 876 + '/auth/.*', 877 + ); 878 + 863 879 return array_mergev($blacklist); 864 880 } 865 881 ··· 903 919 ->setContent($content); 904 920 } else { 905 921 $content = $this->render(); 922 + 906 923 $response = id(new AphrontWebpageResponse()) 907 924 ->setContent($content) 908 925 ->setFrameable($this->getFrameable()); 926 + 927 + $static = CelerityAPI::getStaticResourceResponse(); 928 + foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) { 929 + foreach ($uris as $uri) { 930 + $response->addContentSecurityPolicyURI($kind, $uri); 931 + } 932 + } 909 933 } 910 934 911 935 return $response;
+30 -1
webroot/rsrc/externals/javelin/core/Stratcom.js
··· 517 517 return len ? this._execContext[len - 1].event : null; 518 518 }, 519 519 520 + initialize: function(initializers) { 521 + var frameable = false; 522 + 523 + for (var ii = 0; ii < initializers.length; ii++) { 524 + var kind = initializers[ii].kind; 525 + var data = initializers[ii].data; 526 + switch (kind) { 527 + case 'behaviors': 528 + JX.initBehaviors(data); 529 + break; 530 + case 'merge': 531 + JX.Stratcom.mergeData(data.block, data.data); 532 + JX.Stratcom.ready = true; 533 + break; 534 + case 'frameable': 535 + frameable = !!data; 536 + break; 537 + } 538 + } 539 + 540 + // If the initializer tags did not explicitly allow framing, framebust. 541 + // This protects us from clickjacking attacks on older versions of IE. 542 + // The "X-Frame-Options" and "Content-Security-Policy" headers provide 543 + // more modern variations of this protection. 544 + if (!frameable) { 545 + if (window.top != window.self) { 546 + window.top.location.replace(window.self.location.href); 547 + } 548 + } 549 + }, 520 550 521 551 /** 522 552 * Merge metadata. You must call this (even if you have no metadata) to ··· 542 572 } else { 543 573 this._data[block] = data; 544 574 if (block === 0) { 545 - JX.Stratcom.ready = true; 546 575 JX.flushHoldingQueue('install-init', function(fn) { 547 576 fn(); 548 577 });
+34 -9
webroot/rsrc/externals/javelin/core/init.js
··· 46 46 makeHoldingQueue('behavior'); 47 47 makeHoldingQueue('install-init'); 48 48 49 - window.__DEV__ = window.__DEV__ || 0; 50 - 51 49 var loaded = false; 52 50 var onload = []; 53 51 var master_event_queue = []; 54 52 var root = document.documentElement; 55 53 var has_add_event_listener = !!root.addEventListener; 56 54 55 + window.__DEV__ = !!root.getAttribute('data-developer-mode'); 56 + 57 57 JX.__rawEventQueue = function(what) { 58 58 master_event_queue.push(what); 59 59 60 - // Evade static analysis - JX.Stratcom 60 + var ii; 61 61 var Stratcom = JX['Stratcom']; 62 - if (Stratcom && Stratcom.ready) { 63 - // Empty the queue now so that exceptions don't cause us to repeatedly 64 - // try to handle events. 62 + 63 + if (!loaded && what.type == 'domready') { 64 + var initializers = []; 65 + 66 + var tags = JX.DOM.scry(document.body, 'data'); 67 + for (ii = 0; ii < tags.length; ii++) { 68 + 69 + // Ignore tags which are not immediate children of the document 70 + // body. If an attacker somehow injects arbitrary tags into the 71 + // content of the document, that should not give them access to 72 + // modify initialization behaviors. 73 + if (tags[ii].parentNode !== document.body) { 74 + continue; 75 + } 76 + 77 + var tag_kind = tags[ii].getAttribute('data-javelin-init-kind'); 78 + var tag_data = tags[ii].getAttribute('data-javelin-init-data'); 79 + tag_data = JX.JSON.parse(tag_data); 80 + 81 + initializers.push({kind: tag_kind, data: tag_data}); 82 + } 83 + 84 + Stratcom.initialize(initializers); 85 + loaded = true; 86 + } 87 + 88 + if (loaded) { 89 + // Empty the queue now so that exceptions don't cause us to repeatedly 90 + // try to handle events. 65 91 var local_queue = master_event_queue; 66 92 master_event_queue = []; 67 - for (var ii = 0; ii < local_queue.length; ++ii) { 93 + for (ii = 0; ii < local_queue.length; ++ii) { 68 94 var evt = local_queue[ii]; 69 95 70 96 // Sometimes IE gives us events which throw when ".type" is accessed; ··· 72 98 // figure out where these are coming from. 73 99 try { var test = evt.type; } catch (x) { continue; } 74 100 75 - if (!loaded && evt.type == 'domready') { 101 + if (evt.type == 'domready') { 76 102 // NOTE: Firefox interprets "document.body.id = null" as the string 77 103 // literal "null". 78 104 document.body && (document.body.id = ''); 79 - loaded = true; 80 105 for (var jj = 0; jj < onload.length; jj++) { 81 106 onload[jj](); 82 107 }