Tim Starling has submitted this change and it was merged.

Change subject: noc: Refactor highlight.php to be simpler and <s>less</s> more 
secure
......................................................................


noc: Refactor highlight.php to be simpler and <s>less</s> more secure

This is a second take at what I started 8 months ago (before this
was in version control) and followed-up on in I2d8a2a0ca8 but was
partially reverted in Idcec674abd due to bug 47112.

The main point of this refactor was to not mix logic with
symlinks and auto-guessting path destinations. Instead glob the
whitelist (the conf directory with symlinks) and only use that.

This was successfully implemented, but when I cleaned it up in
I2d8a2a0ca8 I made a stupid typo of a variable name: The foreach
loop variable $viewFilename of $viewFilenames conflicted with
the equally-named variable outside the loop that was supposed to
represent the sanitised version that is considered safe and in
the whitelist. As a result bug 47112.

I've now re-implemented this (without the typo, obviously) and
made it even stricter by using the whitelist exclusively for any
resolution. Not just to filter the user input, but also to
open the file (by reading the symlink).

This completely gets rid the logic that tries to find the file
in /home/wikipedia/common, which is why previously a simple typo
could cause bug 47112.

Also picked more descriptive variable names and added unit tests.

Change-Id: I56b455ebb8e3e083bb6390e03e350b69b902bc64
---
M docroot/noc/conf/highlight.php
A tests/noc-conf/highlightTest.php
2 files changed, 161 insertions(+), 43 deletions(-)

Approvals:
  Tim Starling: Verified; Looks good to me, approved
  jenkins-bot: Checked



diff --git a/docroot/noc/conf/highlight.php b/docroot/noc/conf/highlight.php
index 05553be..02446d1 100644
--- a/docroot/noc/conf/highlight.php
+++ b/docroot/noc/conf/highlight.php
@@ -1,81 +1,93 @@
 <?php
 // Only allow viewing of files of which there is a copy (or link)
 // in noc/conf/* by the same name.
-$viewFilenames = array_map( 'basename', glob( __DIR__ . '/*' ) );
+$selectableFilepaths = glob( __DIR__ . '/*' );
 
-$srcFilename = $_GET['file'];
+// Name of file from user input
+$selectedFileName = $_GET['file'];
 
-$viewFilename = false;
-$srcDir = false;
+// Absolute path to file on disk
+$selectedFilePath = false;
 
-foreach ( $viewFilenames as $view ) {
-       $src = substr( $view, -4 ) === '.txt'
-               ? substr( $view, 0, -4 )
-               : $view;
-       if ( $srcFilename === $src ) {
-               $viewFilename = $view;
+// Path to file in mediawiki-config repository
+$selectedFileRepoDirName = false;
+$selectedFileRepoPath = false;
+
+// Relative path to the symlink in conf/*
+$selectedFileViewRawUrl = false;
+
+foreach ( $selectableFilepaths as $filePath ) {
+       $fileName = basename( $filePath );
+       // Map .txt links to the original filename
+       if ( substr( $fileName, -4 ) === '.txt' ) {
+               $fileName =  substr( $fileName, 0, -4 );
+       }
+       if ( $fileName === $selectedFileName ) {
+               $selectedFilePath = $filePath;
                break;
        }
 }
+if ( PHP_SAPI !== 'cli' ) {
+       ob_start( 'ob_gzhandler' );
+       header( 'Content-Type: text/html; charset=utf-8' );
+}
 
-ob_start( 'ob_gzhandler' );
-header( 'Content-Type: text/html; charset=utf-8' );
-
-if ( !$viewFilename ) {
-       # Secret site password distribution :-D
-       # First implement access control
-       if ( isset( $_SERVER['HTTP_REFERER'] ) && strpos( 
$_SERVER['HTTP_REFERER'], 'google' ) !== false ) {
+if ( !$selectedFilePath ) {
+       if ( PHP_SAPI !== 'cli' ) {
                header( "HTTP/1.1 404 Not Found" );
+       }
+       if ( isset( $_SERVER['HTTP_REFERER'] ) && strpos( strtolower( 
$_SERVER['HTTP_REFERER'] ), 'google' ) !== false ) {
                echo "File not found\n";
                exit;
        }
-       # OK, authenticated developer, send password
+       // Easter egg
        $hlHtml = highlight_string( '<'."?php\n\$secretSitePassword = 
'jgmeidj28gms';\n", true );
+
 } else {
-       $baseSrcDir = '/home/wikipedia/common';
+       // Follow symlink
+       if ( !file_exists( $selectedFilePath ) ) {
+               $hlHtml = 'Whitelist contains inexistant entry. :(';
+       } elseif ( !is_link( $selectedFilePath ) ) {
+               $hlHtml = 'Whitelist must only contain symlinks.';
+       } else {
+               $selectedFileViewRawUrl = './' . urlencode( basename( 
$selectedFilePath ) );
+               // Resolve symlink
+               // Don't use readlink since that will return a path relative to 
where the symlink is.
+               // Which is a problem if our PWD is not the same dir (such as 
in unit tests).
+               $selectedFilePath = realpath( $selectedFilePath );
+               // Figure out path to selected file in the mediawiki-config 
repository
+               $selectedFileRepoPath = ( dirname( $selectedFilePath ) === 
'wmf-config' ? 'wmf-config/' : '' ) . $selectedFileName;
 
-       // Find where it is
-       if ( file_exists( "$baseSrcDir/wmf-config/$srcFilename" ) ) {
-               $srcPath = "$baseSrcDir/wmf-config/$srcFilename";
-               $srcDir = 'wmf-config/';
-       } elseif ( file_exists( "$baseSrcDir/$srcFilename" ) ) {
-               $srcPath = "$baseSrcDir/$srcFilename";
-               $srcDir = '';
-       }
+               if ( substr( $selectedFileName, -4 ) === '.php' ) {
 
-       // How to display it
-       if ( $srcDir !== false ) {
-               if ( substr( $srcFilename, -4 ) === '.php' ) {
-
-                       $hlHtml = highlight_file( $srcPath, true );
+                       $hlHtml = highlight_file( $selectedFilePath, true );
                        $hlHtml = str_replace( '&nbsp;', ' ', $hlHtml ); // 
https://bugzilla.wikimedia.org/19253
                        $hlHtml = str_replace( '    ', "\t", $hlHtml ); // 
convert 4 spaces to 1 tab character; bug #36576
                } else {
-                       $hlHtml = htmlspecialchars( file_get_contents( __DIR__ 
. '/' . $srcFilename ) );
+                       $hlHtml = htmlspecialchars( file_get_contents( 
$selectedFilePath ) );
                }
-       } else {
-               $hlHtml = 'Failed to read file. :(';
        }
 
        $hlHtml = "<pre>$hlHtml</pre>";
 }
 
-$titleSrcFilename = htmlspecialchars( $srcFilename );
-$urlSrcFilename = htmlspecialchars( urlencode( $srcDir . $srcFilename ) );
-$urlViewFilename = htmlspecialchars( urlencode( $viewFilename ) );
+$selectedFileNameEsc = htmlspecialchars( $selectedFileName );
+$selectedFileRepoPathEsc = htmlspecialchars( $selectedFileRepoPath );
+$selectedFileViewRawUrlEsc = htmlspecialchars( $selectedFileViewRawUrl );
 ?>
 <!DOCTYPE html>
 <html lang="en">
 <head>
-       <title><?php echo $titleSrcFilename; ?></title>
-       <link rel="stylesheet" href="/base.css">
+       <title><?php echo $selectedFileNameEsc; ?> - Wikimedia configuration 
files</title>
+       <link rel="shortcut icon" href="//bits.wikimedia.org/favicon/wmf.ico">
+       <link rel="stylesheet" href="../base.css">
 </head>
 <body>
-<h1><a href="./">&laquo;</a> <?php echo $titleSrcFilename; ?></h1>
+<h1><a href="./">&laquo;</a> <?php echo $selectedFileNameEsc; ?></h1>
 <?php
-if ( $srcDir !== false ) :
+if ( $selectedFilePath !== false ) :
 ?>
-<p>(<a 
href="https://gerrit.wikimedia.org/r/gitweb?p=operations/mediawiki-config.git;a=history;f=<?php
 echo $urlSrcFilename; ?>;hb=HEAD">version control</a> &bull; <a 
href="https://gerrit.wikimedia.org/r/gitweb?p=operations/mediawiki-config.git;a=blame;f=<?php
 echo $urlSrcFilename; ?>;hb=HEAD">blame</a> &bull; <a href="./<?php echo 
$urlViewFilename; ?>">raw text</a>)</p>
+<p>(<a 
href="https://gerrit.wikimedia.org/r/gitweb?p=operations/mediawiki-config.git;a=history;f=<?php
 echo $selectedFileRepoPathEsc; ?>;hb=HEAD">version control</a> &bull; <a 
href="https://gerrit.wikimedia.org/r/gitweb?p=operations/mediawiki-config.git;a=blame;f=<?php
 echo $selectedFileRepoPathEsc; ?>;hb=HEAD">blame</a> &bull; <a href="<?php 
echo $selectedFileViewRawUrlEsc; ?>">raw text</a>)</p>
 <?php
 endif;
 ?>
diff --git a/tests/noc-conf/highlightTest.php b/tests/noc-conf/highlightTest.php
new file mode 100644
index 0000000..ddd0a65
--- /dev/null
+++ b/tests/noc-conf/highlightTest.php
@@ -0,0 +1,106 @@
+<?php
+
+class NocConfHighlightTest extends PHPUnit_Framework_TestCase {
+       private $created = array();
+
+       protected function setUp() {
+               parent::setUp();
+
+               $wmfConfigDir = dirname( dirname( __DIR__ ) ) . '/wmf-config';
+               $this->nocConfDir = dirname( dirname( __DIR__ ) ) . 
'/docroot/noc/conf';
+
+               // Created various files to test with
+               if ( !file_exists( "$wmfConfigDir/AdminSettings.php" ) ) {
+                       $this->created[] = "$wmfConfigDir/AdminSettings.php";
+                       file_put_contents( "$wmfConfigDir/AdminSettings.php", 
'<?php $forbiddenFruit = "a";' );
+               }
+
+               if ( !file_exists( "$wmfConfigDir/PrivateSettings.php" ) ) {
+                       $this->created[] = "$wmfConfigDir/PrivateSettings.php";
+                       file_put_contents( "$wmfConfigDir/PrivateSettings.php", 
'<?php $forbiddenFruit = "p";' );
+               }
+
+               $this->created[] = "$wmfConfigDir/ExampleValid.php";
+               file_put_contents( "$wmfConfigDir/ExampleValid.php", '<?php 
$smoigel = "v";' );
+
+               $this->created[] = "$wmfConfigDir/ExampleInvalid.php";
+               file_put_contents( "$wmfConfigDir/ExampleInvalid.php", '<?php 
$forbiddenFruit = "x";' );
+
+               $this->created[] = "{$this->nocConfDir}/ExampleContent.php.txt";
+               file_put_contents( 
"{$this->nocConfDir}/ExampleContent.php.txt", '<?php forbiddenFruit = 
"content";' );
+
+               $this->created[] = "{$this->nocConfDir}/ExampleContent.txt";
+               file_put_contents( "{$this->nocConfDir}/ExampleContent.txt", 
'forbiddenFruit=txt-content' );
+
+               $this->created[] = "{$this->nocConfDir}/ExampleContent";
+               file_put_contents( "{$this->nocConfDir}/ExampleContent", 
'forbiddenFruit=content' );
+
+               $this->created[] = "{$this->nocConfDir}/ExampleValid.php.txt";
+               symlink( "$wmfConfigDir/ExampleValid.php", 
"{$this->nocConfDir}/ExampleValid.php.txt" );
+       }
+
+       protected function tearDown() {
+               foreach ( $this->created as $created ) {
+                       unlink( $created );
+               }
+               parent::tearDown();
+       }
+
+       public static function provideValidCases() {
+               return array(
+                       array( 'langlist', 'zh-classical', 'From root, without 
extension' ),
+                       array( 'all.dblist', 'enwiki', 'From root, dblist file' 
),
+                       array( 'ExampleValid.php', 'smoigel', 'From wmf-config, 
dblist file' ),
+               );
+       }
+
+       /**
+        * @dataProvider provideValidCases
+        */
+       public function testValidCases( $q, $expect, $msg) {
+               $this->assertContains(
+                       $expect,
+                       $this->runHighlight( $q ),
+                       "file=$q should work ($msg)"
+               );
+       }
+
+       public static function provideInvalidCases() {
+               return array(
+                       array( 'search-redirect.php' ),
+                       array( 'robots.txt' ),
+                       array( 'README' ),
+                       array( 'wmf-config/AdminSettings.php' ),
+                       array( 'wmf-config/PrivateSettings.php' ),
+                       array( 'wmf-config/ExampleFile.php' ),
+                       array( 'AdminSettings.php' ),
+                       array( 'PrivateSettings.php' ),
+                       array( 'ExampleInvalid.php' ),
+                       array( 'ExampleContent.php', 'must only contain 
symlinks' ),
+                       array( 'ExampleContent', 'must only contain symlinks' ),
+                       array( 'ExampleContent.txt' ),
+               );
+       }
+
+       /**
+        * @dataProvider provideInvalidCases
+        */
+       public function testInvalidCases( $q, $expect = 'secretSitePassword' ) {
+               $this->assertContains(
+                       $expect,
+                       $this->runHighlight( $q ),
+                       "file=$q should be 404"
+               );
+       }
+
+       /** @return string Page output */
+       protected function runHighlight( $q ) {
+               $_GET = array(
+                       'file' => $q
+               );
+               ob_start();
+               require( $this->nocConfDir . '/highlight.php' );
+               $out = ob_get_clean();
+               return $out;
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/59034
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I56b455ebb8e3e083bb6390e03e350b69b902bc64
Gerrit-PatchSet: 7
Gerrit-Project: operations/mediawiki-config
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>
Gerrit-Reviewer: Hashar <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: MZMcBride <[email protected]>
Gerrit-Reviewer: Tim Starling <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to