r103771 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103770‎ | r103771 | r103772 >
Date:18:48, 20 November 2011
Author:gicode
Status:deferred
Tags:security 
Comment:
Fixes for bug 32154. Improves security of CSS extension quite a bit.
Modified paths:
  • /trunk/extensions/CSS/CSS.php (modified) (history)
  • /trunk/extensions/CSS/verifyCSSLoad.js (added) (history)

Diff [purge]

Index: trunk/extensions/CSS/verifyCSSLoad.js
@@ -0,0 +1,362 @@
 2+/**
 3+ * This file handles fallback for link elements with data URIs generated by
 4+ * this extension. Generally it will only affect IE6/7. However, we do not
 5+ * sniff for browser name or version, so other non-compliant browsers should be
 6+ * handled as well.
 7+ *
 8+ * @file
 9+ * @ingroup Extensions
 10+ * @author Rusty Burchfield
 11+ * @copyright © 2011 Rusty Burchfield
 12+ * @licence The MIT License
 13+ */
 14+
 15+(function( $ ) {
 16+ $.fn.cssExtensionDataURIFallback = function( id, color ) {
 17+ // Install the canary and set its inital color
 18+ var canary = $( '<div>' )
 19+ .attr( 'id', id )
 20+ .css( 'display', 'none' )
 21+ .css( 'background-color', '#FFFFFF' )
 22+ .appendTo( 'body' );
 23+
 24+ var desiredColor = new RGBColor( color );
 25+ var actualColor = canary.css( 'background-color' );
 26+ actualColor = new RGBColor( actualColor );
 27+ canary.remove();
 28+
 29+ // Did the canary change color as expected?
 30+ if ( desiredColor.toHex() == actualColor.toHex() ) {
 31+ return;
 32+ }
 33+
 34+ // Fallback to decoding href and inserting style tag
 35+ var code = this.attr( 'href' ).replace( /^.*;base64,/, '' );
 36+ var text = Base64.decode( code );
 37+
 38+ var style = $( '<style>' ).attr( 'type', 'text/css' );
 39+ if ( style.get( 0 ).styleSheet ) {
 40+ // IE6/7 (our main fallback targets) only work this way.
 41+ style.get( 0 ).styleSheet.cssText = text;
 42+ } else {
 43+ style.text( text );
 44+ }
 45+ style.appendTo( 'head' );
 46+ };
 47+
 48+ /*
 49+ * Base64 decode
 50+ * @link http://www.webtoolkit.info/javascript-base64.html
 51+ * @license http://www.webtoolkit.info/license
 52+ */
 53+ var Base64 = {
 54+ // private property
 55+ _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 56+
 57+ // public method for decoding
 58+ decode : function(input) {
 59+ var output = "";
 60+ var chr1, chr2, chr3;
 61+ var enc1, enc2, enc3, enc4;
 62+ var i = 0;
 63+
 64+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
 65+
 66+ while (i < input.length) {
 67+
 68+ enc1 = this._keyStr.indexOf(input.charAt(i++));
 69+ enc2 = this._keyStr.indexOf(input.charAt(i++));
 70+ enc3 = this._keyStr.indexOf(input.charAt(i++));
 71+ enc4 = this._keyStr.indexOf(input.charAt(i++));
 72+
 73+ chr1 = (enc1 << 2) | (enc2 >> 4);
 74+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
 75+ chr3 = ((enc3 & 3) << 6) | enc4;
 76+
 77+ output = output + String.fromCharCode(chr1);
 78+
 79+ if (enc3 != 64) {
 80+ output = output + String.fromCharCode(chr2);
 81+ }
 82+ if (enc4 != 64) {
 83+ output = output + String.fromCharCode(chr3);
 84+ }
 85+ }
 86+
 87+ output = Base64._utf8_decode(output);
 88+ return output;
 89+ },
 90+
 91+ // private method for UTF-8 decoding
 92+ _utf8_decode : function(utftext) {
 93+ var string = "";
 94+ var i = 0;
 95+ var c = c1 = c2 = 0;
 96+
 97+ while ( i < utftext.length ) {
 98+ c = utftext.charCodeAt(i);
 99+
 100+ if (c < 128) {
 101+ string += String.fromCharCode(c);
 102+ i++;
 103+ }
 104+ else if((c > 191) && (c < 224)) {
 105+ c2 = utftext.charCodeAt(i+1);
 106+ string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
 107+ i += 2;
 108+ }
 109+ else {
 110+ c2 = utftext.charCodeAt(i+1);
 111+ c3 = utftext.charCodeAt(i+2);
 112+ string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
 113+ i += 3;
 114+ }
 115+ }
 116+ return string;
 117+ }
 118+ }
 119+
 120+ /*
 121+ * A class to parse color values
 122+ * @author Stoyan Stefanov <sstoo@gmail.com>
 123+ * @link http://www.phpied.com/rgb-color-parser-in-javascript/
 124+ * @license Use it if you like it
 125+ */
 126+ function RGBColor(color_string) {
 127+ this.ok = false;
 128+
 129+ // strip any leading #
 130+ if (color_string.charAt(0) == '#') { // remove # if any
 131+ color_string = color_string.substr(1,6);
 132+ }
 133+
 134+ color_string = color_string.replace(/ /g,'');
 135+ color_string = color_string.toLowerCase();
 136+
 137+ // before getting into regexps, try simple matches
 138+ // and overwrite the input
 139+ var simple_colors = {
 140+ aliceblue: 'f0f8ff',
 141+ antiquewhite: 'faebd7',
 142+ aqua: '00ffff',
 143+ aquamarine: '7fffd4',
 144+ azure: 'f0ffff',
 145+ beige: 'f5f5dc',
 146+ bisque: 'ffe4c4',
 147+ black: '000000',
 148+ blanchedalmond: 'ffebcd',
 149+ blue: '0000ff',
 150+ blueviolet: '8a2be2',
 151+ brown: 'a52a2a',
 152+ burlywood: 'deb887',
 153+ cadetblue: '5f9ea0',
 154+ chartreuse: '7fff00',
 155+ chocolate: 'd2691e',
 156+ coral: 'ff7f50',
 157+ cornflowerblue: '6495ed',
 158+ cornsilk: 'fff8dc',
 159+ crimson: 'dc143c',
 160+ cyan: '00ffff',
 161+ darkblue: '00008b',
 162+ darkcyan: '008b8b',
 163+ darkgoldenrod: 'b8860b',
 164+ darkgray: 'a9a9a9',
 165+ darkgreen: '006400',
 166+ darkkhaki: 'bdb76b',
 167+ darkmagenta: '8b008b',
 168+ darkolivegreen: '556b2f',
 169+ darkorange: 'ff8c00',
 170+ darkorchid: '9932cc',
 171+ darkred: '8b0000',
 172+ darksalmon: 'e9967a',
 173+ darkseagreen: '8fbc8f',
 174+ darkslateblue: '483d8b',
 175+ darkslategray: '2f4f4f',
 176+ darkturquoise: '00ced1',
 177+ darkviolet: '9400d3',
 178+ deeppink: 'ff1493',
 179+ deepskyblue: '00bfff',
 180+ dimgray: '696969',
 181+ dodgerblue: '1e90ff',
 182+ feldspar: 'd19275',
 183+ firebrick: 'b22222',
 184+ floralwhite: 'fffaf0',
 185+ forestgreen: '228b22',
 186+ fuchsia: 'ff00ff',
 187+ gainsboro: 'dcdcdc',
 188+ ghostwhite: 'f8f8ff',
 189+ gold: 'ffd700',
 190+ goldenrod: 'daa520',
 191+ gray: '808080',
 192+ green: '008000',
 193+ greenyellow: 'adff2f',
 194+ honeydew: 'f0fff0',
 195+ hotpink: 'ff69b4',
 196+ indianred : 'cd5c5c',
 197+ indigo : '4b0082',
 198+ ivory: 'fffff0',
 199+ khaki: 'f0e68c',
 200+ lavender: 'e6e6fa',
 201+ lavenderblush: 'fff0f5',
 202+ lawngreen: '7cfc00',
 203+ lemonchiffon: 'fffacd',
 204+ lightblue: 'add8e6',
 205+ lightcoral: 'f08080',
 206+ lightcyan: 'e0ffff',
 207+ lightgoldenrodyellow: 'fafad2',
 208+ lightgrey: 'd3d3d3',
 209+ lightgreen: '90ee90',
 210+ lightpink: 'ffb6c1',
 211+ lightsalmon: 'ffa07a',
 212+ lightseagreen: '20b2aa',
 213+ lightskyblue: '87cefa',
 214+ lightslateblue: '8470ff',
 215+ lightslategray: '778899',
 216+ lightsteelblue: 'b0c4de',
 217+ lightyellow: 'ffffe0',
 218+ lime: '00ff00',
 219+ limegreen: '32cd32',
 220+ linen: 'faf0e6',
 221+ magenta: 'ff00ff',
 222+ maroon: '800000',
 223+ mediumaquamarine: '66cdaa',
 224+ mediumblue: '0000cd',
 225+ mediumorchid: 'ba55d3',
 226+ mediumpurple: '9370d8',
 227+ mediumseagreen: '3cb371',
 228+ mediumslateblue: '7b68ee',
 229+ mediumspringgreen: '00fa9a',
 230+ mediumturquoise: '48d1cc',
 231+ mediumvioletred: 'c71585',
 232+ midnightblue: '191970',
 233+ mintcream: 'f5fffa',
 234+ mistyrose: 'ffe4e1',
 235+ moccasin: 'ffe4b5',
 236+ navajowhite: 'ffdead',
 237+ navy: '000080',
 238+ oldlace: 'fdf5e6',
 239+ olive: '808000',
 240+ olivedrab: '6b8e23',
 241+ orange: 'ffa500',
 242+ orangered: 'ff4500',
 243+ orchid: 'da70d6',
 244+ palegoldenrod: 'eee8aa',
 245+ palegreen: '98fb98',
 246+ paleturquoise: 'afeeee',
 247+ palevioletred: 'd87093',
 248+ papayawhip: 'ffefd5',
 249+ peachpuff: 'ffdab9',
 250+ peru: 'cd853f',
 251+ pink: 'ffc0cb',
 252+ plum: 'dda0dd',
 253+ powderblue: 'b0e0e6',
 254+ purple: '800080',
 255+ red: 'ff0000',
 256+ rosybrown: 'bc8f8f',
 257+ royalblue: '4169e1',
 258+ saddlebrown: '8b4513',
 259+ salmon: 'fa8072',
 260+ sandybrown: 'f4a460',
 261+ seagreen: '2e8b57',
 262+ seashell: 'fff5ee',
 263+ sienna: 'a0522d',
 264+ silver: 'c0c0c0',
 265+ skyblue: '87ceeb',
 266+ slateblue: '6a5acd',
 267+ slategray: '708090',
 268+ snow: 'fffafa',
 269+ springgreen: '00ff7f',
 270+ steelblue: '4682b4',
 271+ tan: 'd2b48c',
 272+ teal: '008080',
 273+ thistle: 'd8bfd8',
 274+ tomato: 'ff6347',
 275+ turquoise: '40e0d0',
 276+ violet: 'ee82ee',
 277+ violetred: 'd02090',
 278+ wheat: 'f5deb3',
 279+ white: 'ffffff',
 280+ whitesmoke: 'f5f5f5',
 281+ yellow: 'ffff00',
 282+ yellowgreen: '9acd32'
 283+ };
 284+ for (var key in simple_colors) {
 285+ if (color_string == key) {
 286+ color_string = simple_colors[key];
 287+ }
 288+ }
 289+ // emd of simple type-in colors
 290+
 291+ // array of color definition objects
 292+ var color_defs = [
 293+ {
 294+ re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
 295+ example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
 296+ process: function(bits) {
 297+ return [
 298+ parseInt(bits[1]),
 299+ parseInt(bits[2]),
 300+ parseInt(bits[3])
 301+ ];
 302+ }
 303+ },
 304+ {
 305+ re: /^(\w{2})(\w{2})(\w{2})$/,
 306+ example: ['#00ff00', '336699'],
 307+ process: function(bits) {
 308+ return [
 309+ parseInt(bits[1], 16),
 310+ parseInt(bits[2], 16),
 311+ parseInt(bits[3], 16)
 312+ ];
 313+ }
 314+ },
 315+ {
 316+ re: /^(\w{1})(\w{1})(\w{1})$/,
 317+ example: ['#fb0', 'f0f'],
 318+ process: function(bits) {
 319+ return [
 320+ parseInt(bits[1] + bits[1], 16),
 321+ parseInt(bits[2] + bits[2], 16),
 322+ parseInt(bits[3] + bits[3], 16)
 323+ ];
 324+ }
 325+ }
 326+ ];
 327+
 328+ // search through the definitions to find a match
 329+ for (var i = 0; i < color_defs.length; i++) {
 330+ var re = color_defs[i].re;
 331+ var processor = color_defs[i].process;
 332+ var bits = re.exec(color_string);
 333+ if (bits) {
 334+ channels = processor(bits);
 335+ this.r = channels[0];
 336+ this.g = channels[1];
 337+ this.b = channels[2];
 338+ this.ok = true;
 339+ }
 340+
 341+ }
 342+
 343+ // validate/cleanup values
 344+ this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
 345+ this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
 346+ this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
 347+
 348+ // some getters
 349+ this.toRGB = function() {
 350+ return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
 351+ }
 352+
 353+ this.toHex = function() {
 354+ var r = this.r.toString(16);
 355+ var g = this.g.toString(16);
 356+ var b = this.b.toString(16);
 357+ if (r.length == 1) r = '0' + r;
 358+ if (g.length == 1) g = '0' + g;
 359+ if (b.length == 1) b = '0' + b;
 360+ return '#' + r + g + b;
 361+ }
 362+ }
 363+})( jQuery );
Index: trunk/extensions/CSS/CSS.php
@@ -16,15 +16,17 @@
1717 * @licence GNU General Public Licence 2.0 or later
1818 */
1919
20 -if ( !defined( 'MEDIAWIKI') ) die('Not an entry point.' );
 20+if ( !defined( 'MEDIAWIKI' ) ) die( 'Not an entry point.' );
2121
22 -define( 'CSS_VERSION', '2.0, 2011-10-27' );
 22+define( 'CSS_VERSION', '3.0, 2011-11-18' );
2323
2424 $wgCSSMagic = 'css';
2525 $wgCSSPath = false;
 26+$wgCSSIdentifier = 'css-extension';
2627
2728 $wgHooks['ParserFirstCallInit'][] = 'wfCSSParserFirstCallInit';
2829 $wgHooks['LanguageGetMagic'][] = 'wfCSSLanguageGetMagic';
 30+$wgHooks['RawPageViewBeforeOutput'][] = 'wfCSSRawPageViewBeforeOutput';
2931
3032 $wgExtensionCredits['parserhook'][] = array(
3133 'path' => __FILE__,
@@ -37,27 +39,73 @@
3840
3941 $wgExtensionMessagesFiles['CSS'] = dirname( __FILE__ ) . '/' . 'CSS.i18n.php';
4042
 43+$wgResourceModules['ext.CSS'] = array(
 44+ 'scripts' => 'verifyCSSLoad.js',
 45+ 'localBasePath' => dirname( __FILE__ ),
 46+ 'remoteExtPath' => 'CSS',
 47+);
 48+
4149 function wfCSSRender( &$parser, $css ) {
42 - global $wgCSSPath, $wgScriptPath;
 50+ global $wgCSSPath, $wgStylePath, $wgCSSIdentifier;
4351
4452 $css = trim( $css );
4553 $title = Title::newFromText( $css );
46 - if ( is_object( $title ) && $title->isKnown() ) {
47 - # "blue link" Article
48 - $url = $title->getLocalURL( 'action=raw&ctype=text/css' );
49 - $headItem = HTML::linkedStyle( $url );
 54+ $rawProtection = "$wgCSSIdentifier=1";
 55+ $headItem = '<!-- Begin Extension:CSS -->';
 56+
 57+ if ( is_object( $title ) && $title->exists() ) {
 58+ # Article actually in the db
 59+ $params = "action=raw&ctype=text/css&$rawProtection";
 60+ $url = $title->getLocalURL( $params );
 61+ $headItem .= HTML::linkedStyle( $url );
5062 } elseif ( $css[0] == '/' ) {
5163 # Regular file
52 - $base = $wgCSSPath === false ? $wgScriptPath : $wgCSSPath;
53 - $headItem = HTML::linkedStyle( $base . $css );
 64+ $base = $wgCSSPath === false ? $wgStylePath : $wgCSSPath;
 65+ $url = wfAppendQuery( $base . $css, $rawProtection );
 66+
 67+ # Verify the expanded URL is still using the base URL
 68+ if ( strpos( wfExpandUrl( $url ), wfExpandUrl( $base ) ) === 0 ) {
 69+ $headItem .= HTML::linkedStyle( $url );
 70+ } else {
 71+ $headItem .= '<!-- Invalid/malicious path -->';
 72+ }
5473 } else {
55 - # Inline CSS
56 - $headItem = HTML::inlineStyle( Sanitizer::checkCss( $css ) );
 74+ # Inline CSS; use data URI to prevent injection. JavaScript
 75+ # will use a canary to verify load and will safely convert to
 76+ # style tag if load fails.
 77+
 78+ # Generate random CSS color that isn't black or white.
 79+ $color = dechex( mt_rand( 1, hexdec( 'fffffe' ) ) );
 80+ $color = str_pad( $color, 6, '0', STR_PAD_LEFT );
 81+
 82+ # Prepend canary CSS to sanitized user CSS
 83+ $canaryId = "$wgCSSIdentifier-canary-$color";
 84+ $canaryCSS = "#$canaryId{background:#$color !important}";
 85+ $css = $canaryCSS . Sanitizer::checkCss( $css );
 86+
 87+ # Encode data URI and append link tag
 88+ $dataPrefix = 'data:text/css;charset=UTF-8;base64,';
 89+ $url = $dataPrefix . base64_encode( $css );
 90+ $headItem .= HTML::linkedStyle( $url );
 91+
 92+ # Calculate URI prefix to match link tag
 93+ $hrefPrefix = $dataPrefix . base64_encode( '#' . $canaryId );
 94+ $hrefPrefix = substr( $url, 0, strlen( $hrefPrefix ) );
 95+
 96+ # Add JS to verify the link tag loaded and fallback if needed
 97+ $parser->getOutput()->addModules( 'ext.CSS' );
 98+ $headItem .= HTML::inlineScript( <<<INLINESCRIPT
 99+jQuery( function( $ ) {
 100+ $( 'link[href^="$hrefPrefix"]' )
 101+ .cssExtensionDataURIFallback( '$canaryId', '$color' );
 102+} );
 103+INLINESCRIPT
 104+ );
57105 }
58106
59 - $headItem = FormatJson::encode( $headItem );
60 - $script = HTML::inlineScript( "$('head').append( $headItem );" );
61 - return array( $script, 'isHTML' => true );
 107+ $headItem .= '<!-- End Extension:CSS -->';
 108+ $parser->getOutput()->addHeadItem( $headItem );
 109+ return '';
62110 }
63111
64112 function wfCSSParserFirstCallInit( $parser ) {
@@ -71,3 +119,11 @@
72120 $magicWords[$wgCSSMagic] = array( $langCode, $wgCSSMagic );
73121 return true;
74122 }
 123+
 124+function wfCSSRawPageViewBeforeOutput( &$rawPage, &$text ) {
 125+ global $wgCSSIdentifier;
 126+ if ( $rawPage->getRequest()->getBool( $wgCSSIdentifier ) ) {
 127+ $text = Sanitizer::checkCss( $text );
 128+ }
 129+ return true;
 130+}

Status & tagging log