Make WordPress Core

Ticket #10237: csp-wip-v1.patch

File csp-wip-v1.patch, 35.3 KB (added by bsterne, 15 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 }