WordPress.org

Make WordPress Core

Ticket #10237: csp-wip-v1.patch

File csp-wip-v1.patch, 35.3 KB (added by bsterne, 5 years ago)

Work in progress. Provides an admin panel to edit CSP and serves header.

  • wp-includes/functions.php

    function get_status_header_desc( $code ) { 
    15871587} 
    15881588 
    15891589/** 
     1590 * Gets the Content Security Policy if specified by the admin. 
     1591 * If CSP is not enabled, then the return value will be false. 
     1592 * 
     1593 * @return string Content Security Policy header value 
     1594 */ 
     1595function get_csp() { 
     1596  if (!get_option("csp_enabled")) 
     1597    return false; 
     1598  $policy = get_option("csp_value"); 
     1599  // use default policy "allow 'self'" if user has not configured a policy 
     1600  if ( !strlen(trim($policy)) ) 
     1601    return "allow 'self'"; 
     1602  else 
     1603    return $policy; 
     1604} 
     1605 
     1606/** 
    15901607 * Set HTTP status header. 
    15911608 * 
    15921609 * @since 2.0.0 
  • wp-includes/classes.php

    class WP { 
    317317         */ 
    318318        function send_headers() { 
    319319                $headers = array('X-Pingback' => get_bloginfo('pingback_url')); 
     320    // Content Security Policy header 
     321    $csp = get_csp(); 
     322    if ($csp) 
     323      $headers["X-Content-Security-Policy"] = $csp; 
     324 
    320325                $status = null; 
    321326                $exit_required = false; 
    322327 
  • wp-includes/script-loader.php

    function wp_default_styles( &$styles ) { 
    473473        $styles->add( 'jcrop', '/wp-includes/js/jcrop/jquery.Jcrop.css', array(), '0.9.8' ); 
    474474        $styles->add( 'imgareaselect', '/wp-includes/js/imgareaselect/imgareaselect.css', array(), '0.9.1' ); 
    475475        $styles->add( 'nav-menu', "/wp-admin/css/nav-menu$suffix.css", array(), '20100322' ); 
     476        $styles->add( 'security', "/wp-admin/css/security.css", array(), '20100412' ); 
    476477 
    477478        foreach ( $rtl_styles as $rtl_style ) { 
    478479                $styles->add_data( $rtl_style, 'rtl', true ); 
  • wp-admin/menu.php.rej

     
     1*************** $menu[80] = array( __('Settings'), 'manage_options 
     2*** 158,166 **** 
     3        $submenu['options-general.php'][25] = array(__('Discussion'), 'manage_options', 'options-discussion.php'); 
     4        $submenu['options-general.php'][30] = array(__('Media'), 'manage_options', 'options-media.php'); 
     5        $submenu['options-general.php'][35] = array(__('Privacy'), 'manage_options', 'options-privacy.php'); 
     6-       $submenu['options-general.php'][40] = array(__('Permalinks'), 'manage_options', 'options-permalink.php'); 
     7        if ( is_super_admin() ) 
     8-               $submenu['options-general.php'][45] = array(__('Miscellaneous'), 'manage_options', 'options-misc.php'); 
     9   
     10  $_wp_last_utility_menu = 80; // The index of the last top-level menu in the utility menu group 
     11   
     12--- 158,167 ---- 
     13        $submenu['options-general.php'][25] = array(__('Discussion'), 'manage_options', 'options-discussion.php'); 
     14        $submenu['options-general.php'][30] = array(__('Media'), 'manage_options', 'options-media.php'); 
     15        $submenu['options-general.php'][35] = array(__('Privacy'), 'manage_options', 'options-privacy.php'); 
     16+       $submenu['options-general.php'][40] = array(__('Security'), 'manage_options', 'options-security.php'); 
     17+       $submenu['options-general.php'][45] = array(__('Permalinks'), 'manage_options', 'options-permalink.php'); 
     18        if ( is_super_admin() ) 
     19+               $submenu['options-general.php'][50] = array(__('Miscellaneous'), 'manage_options', 'options-misc.php'); 
     20   
     21  $_wp_last_utility_menu = 80; // The index of the last top-level menu in the utility menu group 
     22   
  • wp-admin/js/options-security.dev.js

     
     1/* 
     2 * Script for Content Security Policy admin page 
     3 * Brandon Sterne <bsterne@mozilla.com>  
     4 * 
     5 * Uses Mark Perkins' jQuery URL Parser plugin documented here 
     6 * http://projects.allmarkedup.com/jquery_url_parser/ 
     7 */ 
     8 
     9// keep a list of hosts for each type of content served on this blog 
     10var sources = { 
     11  'images': {}, 'media': {}, 'script': {}, 'object': {}, 
     12  'frame': {},  'font': {},  'style': {} 
     13}; 
     14 
     15// store mappings between content-types and CSP directive names 
     16var directives = { 
     17  'images': 'img-src', 'media': 'media-src', 'script': 'script-src', 
     18  'object': 'object-src', 'frame': 'frame-src',  'font': 'font-src', 
     19  'style': 'style-src' 
     20}; 
     21// allows us to look up directive mappings in either direction 
     22directives.invert = function() { 
     23  var inverted = {}; 
     24  for (var prop in this) { 
     25    if (prop != "invert") inverted[this[prop]] = prop; 
     26  } 
     27  return inverted; 
     28} 
     29 
     30// Douglas Crockford's trim() implementation 
     31String.prototype.trim = function () { 
     32  return this.replace(/^\s+|\s+$/g, ""); 
     33}; 
     34 
     35// return a word with the first letter capitalized 
     36function capWord(w) { 
     37  return w.charAt(0).toUpperCase() + w.slice(1); 
     38} 
     39 
     40// return whether or not an object has no properties 
     41function objIsEmpty(obj) { 
     42  for (var prop in obj) { 
     43    if (obj.hasOwnProperty(prop)) 
     44      return false; 
     45  } 
     46  return true; 
     47} 
     48 
     49// show debugging info 
     50var DEBUG = false; 
     51 
     52// for debugging: return a pretty-printed version of the content sources 
     53function display(sources) { 
     54  var s = "{\n"; 
     55  for (type in sources) { 
     56    s += "  "+type+": ["; 
     57    for (host in sources[type]) { 
     58      s += host+", "; 
     59    } 
     60    s += "],\n"; 
     61  } 
     62  s += "}"; 
     63  return s; 
     64} 
     65 
     66// page loaded 
     67jQuery(document).ready(function($) { 
     68  // if there is an existing policy saved we will display it visually 
     69  var policy = $("#csp_value").val(); 
     70  // CSP is enabled, but not customized.  The default policy is to allow content 
     71  // only from this site 
     72  if (policy == "allow 'self';") { 
     73    var myHost = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     74    // show non-editable policy representation.  we don't want them to be able 
     75    // to remove "allow 'self'" (at least through the visual policy editor). 
     76    showVisualPolicy({"allowing":{"'self'":null}}, "csp_display", false); 
     77  } 
     78  // policy has been customized by the user 
     79  else if (policy.length) { 
     80    // parse the CSP syntax into our JS object format 
     81    var myDirs = policy.split(";");  // current CSP directives 
     82    // directive-to-readable-name mappings 
     83    var invDirectives = directives.invert(); 
     84    for (var i = 0 ; i < myDirs.length ; i++) { 
     85      var dirParts = myDirs[i].trim().split(/[\s]+/); 
     86      // only process directives we know about 
     87      if (dirParts[0] in invDirectives) { 
     88        // add hosts allowed for this directive 
     89        for (var j = 1 ; j < dirParts.length ; j++) 
     90          sources[ invDirectives[dirParts[0]] ][dirParts[j]] = null; 
     91      } 
     92    } 
     93    if (DEBUG) $("#log").text(display(sources));     
     94    // show editable list of policy sources 
     95    showVisualPolicy(sources, "csp_display", true); 
     96  } 
     97  // no CSP set 
     98  else { 
     99    $("#csp_display").html("<p>&nbsp;</p>"); 
     100  } 
     101 
     102  // discard changes to the policy settings by reloading the page 
     103  $("#csp_discard").click( function() { 
     104    window.location.reload(true); 
     105  }); 
     106 
     107  // show/hide the advanced panel for manual editing 
     108  $("#csp_toggle_advanced").click( function() { 
     109    if ($(this).text() == "Show Advanced") { 
     110      $("#csp_advanced").show(); 
     111      $(this).text("Hide Advanced"); 
     112    } 
     113    else { 
     114      $("#csp_advanced").hide(); 
     115      $(this).text("Show Advanced"); 
     116    } 
     117  }); 
     118 
     119  // fetch the front page of the blog to analyze for Content Security 
     120  // Policy suggestion 
     121  $("#suggestpolicy").click( function() { 
     122    $("#analyze").attr("src", location.href.replace(/\/[^\/]+\/[^\/]+$/,"")); 
     123    // show throbber while analyzing 
     124    $("#content-loading").css("visibility", "visible"); 
     125    return false; 
     126  }); 
     127 
     128  // when "Enable CSP" preference is toggeld, enable "Discard Changes" button 
     129  $("#csp_enabled").click( function() { 
     130    $("#csp_discard").show(); 
     131    // if CSP is enabled, ensure that the source list contains at least 
     132    // the site itself, since "allow 'self'" is the default policy 
     133    if ($(this).attr("checked")) { 
     134      var sourcesIsEmpty = true; 
     135      for (type in sources) { 
     136        if (!objIsEmpty(sources[type])) { 
     137          sourcesIsEmpty = false; 
     138          break; 
     139        } 
     140      } 
     141      // show non-editable policy representation.  we don't want them to be able 
     142      // to remove "allow 'self'" (at least through the visual policy editor). 
     143      if (sourcesIsEmpty) 
     144        showVisualPolicy({"allowing":{"'self'":null}}, "csp_display", false); 
     145    } 
     146  }); 
     147 
     148  // once the front page has loaded, examine its content for policy suggestions 
     149  $("#analyze").load(function() { 
     150    // don't examine until we've loaded a page in the frame 
     151    if (!$(this).attr("src")) 
     152      return false; 
     153 
     154    // use 'self' for content from this host 
     155    var myHost = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     156 
     157    /* images */ 
     158    // XXX bsterne - nearly every blog I've looked at sources images from 
     159    // all over the place.  It's potentially confusing to users to show a list 
     160    // containing every site they get images from.  For now, will allow 
     161    // images from anywhere. 
     162    sources.images["*"] = null; 
     163    /* 
     164    $.each($(this).contents().find("img"), function() { 
     165      var host = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     166      // relative URL, use 'self' 
     167      if (host == null) { 
     168        if (!(host in sources.images)) 
     169          sources.images["'self'"] = null; 
     170      } 
     171      // absolute URL, store the hostname 
     172      else { 
     173        if (!(host in sources.images)) 
     174          sources.images[host] = null; 
     175      } 
     176    }); 
     177    */ 
     178 
     179    /* media: <video> and <audio> */ 
     180    $.each($(this).contents().find("video,audio"), function() { 
     181      var host = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     182      // relative URL, use 'self' 
     183      if (host == null) { 
     184        if (!(host in sources.media)) 
     185          sources.media["'self'"] = null; 
     186      } 
     187      // absolute URL, store the hostname 
     188      else { 
     189        if (host == myHost) 
     190          host = "'self'"; 
     191        if (!(host in sources.media)) 
     192          sources.media[host] = null; 
     193      } 
     194    }); 
     195 
     196    /* external script resources */ 
     197    $.each($(this).contents().find("script"), function() { 
     198      var host = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     199      // relative URL, use 'self' 
     200      if (host == null) { 
     201        if (!(host in sources.script)) 
     202          sources.script["'self'"] = null; 
     203      } 
     204      // absolute URL, store the hostname 
     205      else { 
     206        if (host == myHost) 
     207          host = "'self'"; 
     208        if (!(host in sources.script)) 
     209          sources.script[host] = null; 
     210      } 
     211    }); 
     212 
     213    /* <object>, <applet>, <embed> */ 
     214    // object, applet 
     215    // http://www.w3.org/TR/1999/REC-html401-19991224/struct/objects.html#h-13.3 
     216    $.each($(this).contents().find("object,applet"), function() { 
     217      // codebase: base URI for classid, data, archive attrs 
     218      var codebase = $(this).attr("codebase"); 
     219      var host = jQuery.url.setUrl(codebase).attr("host"); 
     220      // relative URL, use 'self' 
     221      if (host == null) { 
     222        if (!(host in sources.object)) 
     223          sources.object["'self'"] = null; 
     224      } 
     225      // absolute URL, store the hostname 
     226      else { 
     227        if (host == myHost) 
     228          host = "'self'"; 
     229        if (!(host in sources.object)) 
     230          sources.object[host] = null; 
     231      } 
     232 
     233      // classid: location of an object's implementation. 
     234      // XXX bsterne - spec says this is a URI, but in the wild this usually 
     235      // references a COM registry ID. For now, skipping this URL for any 
     236      // non-data-returning protocols, e.g. clsid:. 
     237      var classid = $(this).attr("classid");  // applet won't have this 
     238      var protocol = jQuery.url.setUrl(classid).attr("protocol"); 
     239      if (jQuery.inArray(protocol, ["http", "https", "ftp", null]) != -1) { 
     240        host = jQuery.url.setUrl(classid).attr("host"); 
     241        // relative URL, use 'self' 
     242        if (host == null) { 
     243          if (!(host in sources.object)) 
     244            sources.object["'self'"] = null; 
     245        } 
     246        // absolute URL, store the hostname 
     247        else { 
     248          if (host == myHost) 
     249            host = "'self'"; 
     250          if (!(host in sources.object)) 
     251            sources.object[host] = null; 
     252        } 
     253      } 
     254 
     255      // data: object's or applet's location 
     256      var data = $(this).attr("data");  // applet won't have this 
     257      host = jQuery.url.setUrl(data).attr("host"); 
     258      // relative URL, use 'self' 
     259      if (host == null) { 
     260        if (!(host in sources.object)) 
     261          sources.object["'self'"] = null; 
     262      } 
     263      // absolute URL, store the hostname 
     264      else { 
     265        if (host == myHost) 
     266          host = "'self'"; 
     267        if (!(host in sources.object)) 
     268          sources.object[host] = null; 
     269      } 
     270 
     271      // archive: space-separated list of URIs of relevant resources 
     272      var archive = $(this).attr("archive"); 
     273      var hosts = archive.split(" "); 
     274      $.each(hosts, function() { 
     275        host = jQuery.url.setUrl(this).attr("host"); 
     276        // relative URL, use 'self' 
     277        if (host == null) { 
     278          if (!(host in sources.object)) 
     279            sources.object["'self'"] = null; 
     280        } 
     281        // absolute URL, store the hostname 
     282        else { 
     283          if (host == myHost) 
     284            host = "'self'"; 
     285          if (!(host in sources.object)) 
     286            sources.object[host] = null; 
     287        } 
     288      }); 
     289    }); 
     290 
     291    // embed 
     292    $.each($(this).contents().find("embed"), function() { 
     293      var host = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     294      // relative URL, use 'self' 
     295      if (host == null) { 
     296        if (!(host in sources.object)) 
     297          sources.object["'self'"] = null; 
     298      } 
     299      // absolute URL, store the hostname 
     300      else { 
     301        if (host == myHost) 
     302          host = "'self'"; 
     303        if (!(host in sources.object)) 
     304          sources.object[host] = null; 
     305      } 
     306    }); 
     307 
     308    // frame, iframe 
     309    $.each($(this).contents().find("frame,iframe"), function() { 
     310      var host = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     311      // relative URL, use 'self' 
     312      if (host == null) { 
     313        if (!(host in sources.frame)) 
     314          sources.frame["'self'"] = null; 
     315      } 
     316      // absolute URL, store the hostname 
     317      else { 
     318        if (host == myHost) 
     319          host = "'self'"; 
     320        if (!(host in sources.frame)) 
     321          sources.frame[host] = null; 
     322      } 
     323    }); 
     324 
     325    // @font-face (downloadable fonts) 
     326    var stylesheets = {}; 
     327    try { 
     328      var stylesheets = this.contentDocument.styleSheets; 
     329    } 
     330    // XXX bsterne - parsing style rules in IE is hard :-( 
     331    // see http://www.quirksmode.org/dom/w3c_css.html 
     332    catch (e) {} 
     333 
     334    for (var i = 0 ; i < stylesheets.length ; i++) { 
     335      // XXX bsterne - apparently, cross-site stylesheets' rules are subject to 
     336      // same-origin.  We'll only be able to make font policy reccommendations 
     337      // based on same-site stylesheets. 
     338      var rules = {}; 
     339      try { 
     340        // Firefox, Chrome, Safari, etc. 
     341        rules = stylesheets[i].cssRules; 
     342        // Internet Explorer would use .rules if we can add support 
     343        // rules = stylesheets[i].rules; 
     344      } 
     345      catch (e) { // probably a cross-site stylesheet which we can't read 
     346        continue; 
     347      } 
     348      // XXX bsterne - Chrome can return null for cssRules but not throw 
     349      if (!rules) { 
     350        continue; 
     351      } 
     352      // search stylesheet rules for @font-face 
     353      for (var j = 0 ; j < rules.length ; j++) { 
     354        if (rules[j].type == rules[j].FONT_FACE_RULE) { 
     355          var src = rules[j].style.getPropertyValue("src"); 
     356          if (src) { 
     357            // remove url() wrapper from font-face src 
     358            // FIXME - get rid of slashes (working around js-mode indent bug) 
     359            var url = src.replace(/^url[\'"]*/, "").replace(/[\'"]*\)$/, ""); 
     360            var host = jQuery.url.setUrl(url).attr("host"); 
     361            // relative URL, use 'self' 
     362            if (host == null) { 
     363              if (!(host in sources.font)) 
     364                sources.font["'self'"] = null; 
     365            } 
     366            // absolute URL, store the hostname 
     367            else { 
     368              if (host == myHost) 
     369                host = "'self'"; 
     370              if (!(host in sources.font)) 
     371                sources.font[host] = null; 
     372            } 
     373          } 
     374        } 
     375      } 
     376    } 
     377 
     378    // external stylesheets 
     379    $.each($(this).contents().find("link[rel='stylesheet']"), function() { 
     380      var host = jQuery.url.setUrl($(this).attr("href")).attr("host"); 
     381      // relative URL, use 'self' 
     382      if (host == null) { 
     383        if (!(host in sources.style)) 
     384          sources.style["'self'"] = null; 
     385      } 
     386      // absolute URL, store the hostname 
     387      else { 
     388        if (host == myHost) 
     389          host = "'self'"; 
     390        if (!(host in sources.style)) 
     391          sources.style[host] = null; 
     392      } 
     393    });     
     394 
     395    // turn off throbber 
     396    $("#content-loading").css("visibility", "hidden"); 
     397 
     398    // display a reccommended policy that the user can edit, then accept or reject 
     399    showVisualPolicy(sources, "csp_display", true); 
     400    // changes were made, enable "Discard Changes" button and change 
     401    // border of "Trusted Sites" panel 
     402    $("#csp_discard").show(); 
     403    $("#csp_display").css("border-style", "dashed").css("border-width","2px"); 
     404  }); 
     405 
     406  // display a visual representation of the policy defined in a source list 
     407  // inside the specified DOM element.  If |editable| is passed in as true, 
     408  // the printed list will be interactive 
     409  function showVisualPolicy(sourceList, elementID, editable) { 
     410    if(DEBUG) $("#log").text(display(sourceList)) 
     411    $("#"+elementID).empty(); 
     412    var myHost = jQuery.url.setUrl($(this).attr("src")).attr("host"); 
     413    for (type in sourceList) { 
     414      var div = document.createElement("div"); 
     415      // skip content types which have no sources 
     416      if (objIsEmpty(sourceList[type])) 
     417        continue; 
     418      $(div).addClass("feature-name").text(capWord(type)); 
     419      $("#"+elementID).append(div); 
     420      var ol = document.createElement("ol"); 
     421      $(ol).addClass("feature-group"); 
     422      for (host in sourceList[type]) { 
     423        var li = document.createElement("li"); 
     424        $(li).attr("host", host); 
     425        $(li).attr("ctype", type); // content type 
     426        // if list is editable, permit the user to remove a source from the 
     427        // suggested source list or add it back in 
     428        if (editable) { 
     429          $(li).click(function() { 
     430            // changes were made, enable "Discard Changes" button and change 
     431            // border of "Trusted Sites" panel 
     432            $("#csp_discard").show(); 
     433            $("#csp_display").css("border-style", "dashed").css("border-width", 
     434                                                                "2px"); 
     435            // re-enable a previously removed source 
     436            if ($(this).css("text-decoration") == "line-through") { 
     437              $(this).css("text-decoration", "none"); 
     438              //sourceList[this.type][this.host] = null; 
     439              sourceList[$(this).attr("ctype")][$(this).attr("host")] = null; 
     440            } 
     441            // remove a source from the list of suggestions 
     442            else { 
     443              $(this).css("text-decoration", "line-through"); 
     444              //delete sourceList[this.type][this.host]; 
     445              delete sourceList[$(this).attr("ctype")][$(this).attr("host")]; 
     446            } 
     447            if (DEBUG) $("#log").text(display(sourceList)); 
     448          }); 
     449        } 
     450        if (host == "*") { 
     451          var displayName = "Everyone"; 
     452          var faviconUrl = "images/everyone.gif"; 
     453        } 
     454        else if (host == "'self'") { 
     455          var displayName = myHost; 
     456          var faviconUrl = "http://" + myHost + "/favicon.ico"; 
     457        } 
     458        else { 
     459          var displayName = host; 
     460          var faviconUrl = "http://" + host + "/favicon.ico"; 
     461        } 
     462        $(li).html("<img src=\"" + faviconUrl + "\" class=\" csp-trusted\"" + 
     463                   " onerror=\"this.parentNode.removeChild(this)\">"); 
     464        if (editable) 
     465          $(li).append("<label>" + displayName + "</label>"); 
     466        else 
     467          $(li).append(displayName); 
     468        $(ol).append(li); 
     469      } 
     470      $("#"+elementID).append(ol).append("<br class=\"clear\">"); 
     471    } 
     472    // show a message describing how to treat the list of sources 
     473    $("#csp_verify_text").show(); 
     474  } 
     475 
     476  // turn a list of sources into proper CSP syntax 
     477  function generatePolicyFromSources(sourceList) { 
     478    var policy = "allow 'self'; "; 
     479    for (type in sourceList) { 
     480      // skip types which have no sources specified 
     481      if (objIsEmpty(sourceList[type])) 
     482        continue; 
     483      policy += directives[type] + " "; 
     484      for (host in sourceList[type]) { 
     485        policy += host+" "; 
     486      } 
     487      policy += "; "; 
     488    } 
     489    return policy; 
     490  } 
     491   
     492  // once the user has verified the list of sources they want to allow, turn the 
     493  // list of hosts-per-content-type into proper CSP syntax 
     494  $("#csp_save").click( function() { 
     495    // if the advanced view is hidden, then generate policy from the visual 
     496    // editor and place in the form field 
     497    if ($("#csp_toggle_advanced").text() == "Show Advanced") { 
     498      $("#csp_value").val( generatePolicyFromSources(sources) ); 
     499    } 
     500    // otherwise the user wants to edit the policy directly, so we'll use what's 
     501    // in the form field 
     502    $("form").submit(); 
     503  }); 
     504 
     505}); 
  • wp-admin/js/jquery.url.js

     
     1/* =========================================================================== 
     2 * 
     3 * JQuery URL Parser 
     4 * Version 1.0 
     5 * Parses URLs and provides easy access to information within them. 
     6 * 
     7 * Author: Mark Perkins 
     8 * Author email: mark@allmarkedup.com 
     9 * 
     10 * For full documentation and more go to http://projects.allmarkedup.com/jquery_url_parser/ 
     11 * 
     12 * --------------------------------------------------------------------------- 
     13 * 
     14 * CREDITS: 
     15 * 
     16 * Parser based on the Regex-based URI parser by Stephen Levithian. 
     17 * For more information (including a detailed explaination of the differences 
     18 * between the 'loose' and 'strict' pasing modes) visit http://blog.stevenlevithan.com/archives/parseuri 
     19 * 
     20 * --------------------------------------------------------------------------- 
     21 * 
     22 * LICENCE: 
     23 * 
     24 * Released under a MIT Licence. See licence.txt that should have been supplied with this file, 
     25 * or visit http://projects.allmarkedup.com/jquery_url_parser/licence.txt 
     26 * 
     27 * --------------------------------------------------------------------------- 
     28 *  
     29 * EXAMPLES OF USE: 
     30 * 
     31 * Get the domain name (host) from the current page URL 
     32 * jQuery.url.attr("host") 
     33 * 
     34 * Get the query string value for 'item' for the current page 
     35 * jQuery.url.param("item") // null if it doesn't exist 
     36 * 
     37 * Get the second segment of the URI of the current page 
     38 * jQuery.url.segment(2) // null if it doesn't exist 
     39 * 
     40 * Get the protocol of a manually passed in URL 
     41 * jQuery.url.setUrl("http://allmarkedup.com/").attr("protocol") // returns 'http' 
     42 * 
     43 */ 
     44 
     45jQuery.url = function() 
     46{ 
     47        var segments = {}; 
     48         
     49        var parsed = {}; 
     50         
     51        /** 
     52    * Options object. Only the URI and strictMode values can be changed via the setters below. 
     53    */ 
     54        var options = { 
     55         
     56                url : window.location, // default URI is the page in which the script is running 
     57                 
     58                strictMode: false, // 'loose' parsing by default 
     59         
     60                key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], // keys available to query  
     61                 
     62                q: { 
     63                        name: "queryKey", 
     64                        parser: /(?:^|&)([^&=]*)=?([^&]*)/g 
     65                }, 
     66                 
     67                parser: { 
     68                        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, // more intuitive, fails on relative paths and deviates from specs 
     69                        loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ //less intuitive, more accurate to the specs 
     70                } 
     71                 
     72        }; 
     73         
     74    /** 
     75     * Deals with the parsing of the URI according to the regex above. 
     76         * Written by Steven Levithan - see credits at top. 
     77     */          
     78        var parseUri = function() 
     79        { 
     80                str = decodeURI( options.url ); 
     81                 
     82                var m = options.parser[ options.strictMode ? "strict" : "loose" ].exec( str ); 
     83                var uri = {}; 
     84                var i = 14; 
     85 
     86                while ( i-- ) { 
     87                        uri[ options.key[i] ] = m[i] || ""; 
     88                } 
     89 
     90                uri[ options.q.name ] = {}; 
     91                uri[ options.key[12] ].replace( options.q.parser, function ( $0, $1, $2 ) { 
     92                        if ($1) { 
     93                                uri[options.q.name][$1] = $2; 
     94                        } 
     95                }); 
     96 
     97                return uri; 
     98        }; 
     99 
     100    /** 
     101     * Returns the value of the passed in key from the parsed URI. 
     102         *  
     103         * @param string key The key whose value is required 
     104     */          
     105        var key = function( key ) 
     106        { 
     107                if ( ! parsed.length ) 
     108                { 
     109                        setUp(); // if the URI has not been parsed yet then do this first...     
     110                }  
     111                if ( key == "base" ) 
     112                { 
     113                        if ( parsed.port !== null && parsed.port !== "" ) 
     114                        { 
     115                                return parsed.protocol+"://"+parsed.host+":"+parsed.port+"/";    
     116                        } 
     117                        else 
     118                        { 
     119                                return parsed.protocol+"://"+parsed.host+"/"; 
     120                        } 
     121                } 
     122         
     123                return ( parsed[key] === "" ) ? null : parsed[key]; 
     124        }; 
     125         
     126        /** 
     127     * Returns the value of the required query string parameter. 
     128         *  
     129         * @param string item The parameter whose value is required 
     130     */          
     131        var param = function( item ) 
     132        { 
     133                if ( ! parsed.length ) 
     134                { 
     135                        setUp(); // if the URI has not been parsed yet then do this first...     
     136                } 
     137                return ( parsed.queryKey[item] === null ) ? null : parsed.queryKey[item]; 
     138        }; 
     139 
     140    /** 
     141     * 'Constructor' (not really!) function. 
     142     *  Called whenever the URI changes to kick off re-parsing of the URI and splitting it up into segments.  
     143     */  
     144        var setUp = function() 
     145        { 
     146                parsed = parseUri(); 
     147                 
     148                getSegments();   
     149        }; 
     150         
     151    /** 
     152     * Splits up the body of the URI into segments (i.e. sections delimited by '/') 
     153     */ 
     154        var getSegments = function() 
     155        { 
     156                var p = parsed.path; 
     157                segments = []; // clear out segments array 
     158                segments = parsed.path.length == 1 ? {} : ( p.charAt( p.length - 1 ) == "/" ? p.substring( 1, p.length - 1 ) : path = p.substring( 1 ) ).split("/"); 
     159        }; 
     160         
     161        return { 
     162                 
     163            /** 
     164             * Sets the parsing mode - either strict or loose. Set to loose by default. 
     165             * 
     166             * @param string mode The mode to set the parser to. Anything apart from a value of 'strict' will set it to loose! 
     167             */ 
     168                setMode : function( mode ) 
     169                { 
     170                        strictMode = mode == "strict" ? true : false; 
     171                        return this; 
     172                }, 
     173                 
     174                /** 
     175             * Sets URI to parse if you don't want to to parse the current page's URI. 
     176                 * Calling the function with no value for newUri resets it to the current page's URI. 
     177             * 
     178             * @param string newUri The URI to parse. 
     179             */          
     180                setUrl : function( newUri ) 
     181                { 
     182                        options.url = newUri === undefined ? window.location : newUri; 
     183                        setUp(); 
     184                        return this; 
     185                },               
     186                 
     187                /** 
     188             * Returns the value of the specified URI segment. Segments are numbered from 1 to the number of segments. 
     189                 * For example the URI http://test.com/about/company/ segment(1) would return 'about'. 
     190                 * 
     191                 * If no integer is passed into the function it returns the number of segments in the URI. 
     192             * 
     193             * @param int pos The position of the segment to return. Can be empty. 
     194             */  
     195                segment : function( pos ) 
     196                { 
     197                        if ( ! parsed.length ) 
     198                        { 
     199                                setUp(); // if the URI has not been parsed yet then do this first...     
     200                        }  
     201                        if ( pos === undefined ) 
     202                        { 
     203                                return segments.length; 
     204                        } 
     205                        return ( segments[pos] === "" || segments[pos] === undefined ) ? null : segments[pos]; 
     206                }, 
     207                 
     208                attr : key, // provides public access to private 'key' function - see above 
     209                 
     210                param : param // provides public access to private 'param' function - see above 
     211                 
     212        }; 
     213         
     214}(); 
  • wp-admin/options-security.php

     
     1<?php 
     2/** 
     3 * Security Settings Administration Panel. 
     4 * 
     5 * @package WordPress 
     6 * @subpackage Administration 
     7 */ 
     8 
     9/** Load WordPress Administration Bootstrap */ 
     10require_once('./admin.php'); 
     11 
     12if ( ! current_user_can('manage_options') ) 
     13        wp_die(__('You do not have sufficient permissions to manage options for this blog.')); 
     14 
     15$title = __('Security Settings'); 
     16$parent_file = 'options-general.php'; 
     17wp_enqueue_style( 'theme-install' ); 
     18wp_enqueue_style( 'security' ); 
     19include('./admin-header.php'); 
     20?> 
     21 
     22<div class="wrap"> 
     23<?php screen_icon(); ?> 
     24<h2><?php echo esc_html( $title ); ?></h2> 
     25 
     26<form method="post" action="options.php"> 
     27<?php settings_fields('security'); ?> 
     28 
     29<h3><?php _e('Content Security Policy'); ?></h3> 
     30<p><a href="https://wiki.mozilla.org/Security/CSP">Content Security Policy</a> is a mechanism that prevents <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">cross-site scripting</a> and other content-injection attacks.  It works by informing the browser regarding which websites you trust to serve content in your pages, and what specific types of content are expected.  To provide protection in this way, CSP fundamentally changes the way JavaScript can be embedded in web pages, so be aware that <b>WordPress plugins may experience broken functionality</b> when CSP is enabled.</p> 
     31<table class="form-table" id="csp-form"> 
     32<tr valign="top"> 
     33<th><label for="csp_enabled"><?php _e('Enable CSP') ?></label></th> 
     34<td><input id="csp_enabled" type="checkbox" name="csp_enabled" value="1" <?php checked('1', get_option('csp_enabled')); ?> /></td> 
     35<th>Trusted Sites:</th> 
     36<td width="80%"> 
     37  <div class="feature-filter" id="csp_display"></div> 
     38</td> 
     39<tr valign="middle"> 
     40<td colspan="3"></td> 
     41<td><span id="csp_verify_text">Verify that you trust the sites in the list above to serve content in your site.  Remove any sites that you do not trust by clicking them.  Your changes will not be saved until you click <b>Save Changes</b> below.</span></td> 
     42</tr> 
     43</tr> 
     44<tr valign="top"> 
     45<td colspan="3"></td> 
     46<td> 
     47  <div class="tablenav" style="vertical-align:bottom"> 
     48    <div class="alignleft actions"><input type="button" name="suggestpolicy" id="suggestpolicy" class="button-secondary" value="<?php esc_attr_e('Suggest Policy') ?>" /><img id="content-loading" alt="" src="images/wpspin_light.gif"><input type="button" class="button-secondary" id="csp_discard" value="<?php esc_attr_e('Discard Changes') ?>" /><input type="button" class="button-primary" id="csp_save" value="<?php esc_attr_e('Save Changes') ?>" /></div> 
     49  </div> 
     50</td> 
     51</tr> 
     52<tr> 
     53<th><label id="csp_toggle_advanced">Show Advanced</label></th> 
     54<td colspan="3"> 
     55  <div class="feature-filter" id="csp_advanced"> 
     56    <p>Do not edit the policy directly unless you really know what you are doing.</p> 
     57    <input type="text" name="csp_value" id="csp_value" value="<?php form_option('csp_value'); ?>" /> 
     58  </div> 
     59</td> 
     60</tr> 
     61<?php do_settings_fields('security', 'default'); ?> 
     62</table> 
     63 
     64<?php do_settings_sections('security'); ?> 
     65 
     66</form> 
     67 
     68</div> 
     69 
     70<iframe id="analyze"></iframe> 
     71<pre id="log"></pre> 
     72<script type="text/javascript" src="js/options-security.dev.js"></script> 
     73<script type="text/javascript" src="js/jquery.url.js"></script> 
     74<?php include('./admin-footer.php') ?> 
  • wp-admin/menu.php

    $menu[80] = array( __('Settings'), 'manage_options 
    198198        $submenu['options-general.php'][25] = array(__('Discussion'), 'manage_options', 'options-discussion.php'); 
    199199        $submenu['options-general.php'][30] = array(__('Media'), 'manage_options', 'options-media.php'); 
    200200        $submenu['options-general.php'][35] = array(__('Privacy'), 'manage_options', 'options-privacy.php'); 
    201         $submenu['options-general.php'][40] = array(__('Permalinks'), 'manage_options', 'options-permalink.php'); 
     201        $submenu['options-general.php'][40] = array(__('Security'), 'manage_options', 'options-security.php'); 
     202        $submenu['options-general.php'][45] = array(__('Permalinks'), 'manage_options', 'options-permalink.php'); 
    202203 
    203204$_wp_last_utility_menu = 80; // The index of the last top-level menu in the utility menu group 
    204205 
  • wp-admin/admin-header.php

     
    66 * @subpackage Administration 
    77 */ 
    88 
     9// Content Security Policy header 
     10$csp = get_csp(); 
     11if ($csp) 
     12  @header("X-Content-Security-Policy: " . $csp); 
    913@header('Content-Type: ' . get_option('html_type') . '; charset=' . get_option('blog_charset')); 
    1014if (!isset($_GET["page"])) require_once('admin.php'); 
    1115 
  • wp-admin/options.php

    Cannot display: file marked as a binary type.
    svn:mime-type = application/octet-stream
    
    Property changes on: wp-admin/images/everyone.gif
    ___________________________________________________________________
    Added: svn:mime-type
       + application/octet-stream
    
    $whitelist_options = array( 
    3838        'discussion' => array( 'default_pingback_flag', 'default_ping_status', 'default_comment_status', 'comments_notify', 'moderation_notify', 'comment_moderation', 'require_name_email', 'comment_whitelist', 'comment_max_links', 'moderation_keys', 'blacklist_keys', 'show_avatars', 'avatar_rating', 'avatar_default', 'close_comments_for_old_posts', 'close_comments_days_old', 'thread_comments', 'thread_comments_depth', 'page_comments', 'comments_per_page', 'default_comments_page', 'comment_order', 'comment_registration' ), 
    3939        'media' => array( 'thumbnail_size_w', 'thumbnail_size_h', 'thumbnail_crop', 'medium_size_w', 'medium_size_h', 'large_size_w', 'large_size_h', 'image_default_size', 'image_default_align', 'image_default_link_type', 'embed_autourls', 'embed_size_w', 'embed_size_h', 'uploads_use_yearmonth_folders', 'upload_path', 'upload_url_path' ), 
    4040        'privacy' => array( 'blog_public' ), 
     41        'security' => array( 'csp_enabled', 'csp_value' ), 
    4142        'reading' => array( 'posts_per_page', 'posts_per_rss', 'rss_use_excerpt', 'blog_charset', 'show_on_front', 'page_on_front', 'page_for_posts' ), 
    4243        'writing' => array( 'default_post_edit_rows', 'use_smilies', 'default_category', 'default_email_category', 'use_balanceTags', 'default_link_category', 'enable_app', 'enable_xmlrpc' ), 
    4344        'options' => array( '' ) ); 
  • wp-admin/css/security.css

     
     1#suggestion, #analyze, #csp_discard, #csp_verify_text, #csp_advanced { display: none } 
     2img.csp-trusted { vertical-align:middle; margin: 0 1em } 
     3#content-loading { visibility:hidden; vertical-align:middle; margin:0 .5em } 
     4#csp_value { width: 75%; margin: 1em }