Brian Wolff has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/395762 )
Change subject: Fix some false positives discovered while testing with MW ...................................................................... Fix some false positives discovered while testing with MW Change-Id: I3bcbd5bbfa7417c490adb2ec7b580e13eaa70741 --- M src/MWVisitor.php M src/MediaWikiSecurityCheckPlugin.php M src/TaintednessBaseVisitor.php M src/TaintednessVisitor.php M tests/integration/arraynumkey/db.php M tests/integration/arraynumkey/expectedResults.txt M tests/integration/arraynumkey/test.php M tests/integration/lists/test.php A tests/integration/lowercase/OutputPage.php A tests/integration/lowercase/expectedResults.txt A tests/integration/lowercase/test.php A tests/integration/ooui/HorizontalLayout.php A tests/integration/ooui/OutputPage.php A tests/integration/ooui/expectedResults.txt A tests/integration/ooui/test.php 15 files changed, 245 insertions(+), 13 deletions(-) diff --git a/src/MWVisitor.php b/src/MWVisitor.php index 24c1328..d7a1533 100644 --- a/src/MWVisitor.php +++ b/src/MWVisitor.php @@ -270,13 +270,16 @@ if ( !$alreadyRegistered ) { // If this is the first time seeing this, re-analyze the // node, just in case we had already passed it by. + $func = null; if ( $callback->isClosure() ) { // For closures we have to reanalyze the parent // function, as we can't reanalyze the closure, and // we definitely need to since the closure would // have already been analyzed at this point since // we are operating in post-order. - $func = $this->context->getFunctionLikeInScope( $this->code_base ); + if ( $this->context->isInFunctionLikeScope() ) { + $func = $this->context->getFunctionLikeInScope( $this->code_base ); + } } elseif ( $callback instanceof FullyQualifiedMethodName ) { $func = $this->code_base->getMethodByFQSEN( $callback ); } else { @@ -286,10 +289,12 @@ // Make sure we reanalyze the hook function now that // we know what it is, in case its already been // analyzed. - $func->analyze( - $func->getContext(), - $this->code_base - ); + if ( $func ) { + $func->analyze( + $func->getContext(), + $this->code_base + ); + } } } diff --git a/src/MediaWikiSecurityCheckPlugin.php b/src/MediaWikiSecurityCheckPlugin.php index 0fa770d..31d7274 100644 --- a/src/MediaWikiSecurityCheckPlugin.php +++ b/src/MediaWikiSecurityCheckPlugin.php @@ -88,30 +88,55 @@ // What should DB results be considered? 'overall' => self::YES_TAINT ], + '\Wikimedia\Rdbms\IMaintainableDatabase::query' => [ + self::SQL_EXEC_TAINT, + // What should DB results be considered? + 'overall' => self::YES_TAINT + ], '\Wikimedia\Rdbms\DBConnRef::query' => [ self::SQL_EXEC_TAINT, // What should DB results be considered? 'overall' => self::YES_TAINT ], '\Wikimedia\Rdbms\IDatabase::select' => $selectWrapper, + '\Wikimedia\Rdbms\IMaintainableDatabase::select' => $selectWrapper, '\Wikimedia\Rdbms\Database::select' => $selectWrapper, '\Wikimedia\Rdbms\DBConnRef::select' => $selectWrapper, '\Wikimedia\Rdbms\IDatabase::selectField' => $selectWrapper, + '\Wikimedia\Rdbms\IMaintainableDatabase::selectField' => $selectWrapper, '\Wikimedia\Rdbms\Database::selectField' => $selectWrapper, '\Wikimedia\Rdbms\DBConnRef::selectField' => $selectWrapper, '\Wikimedia\Rdbms\IDatabase::selectFieldValues' => $selectWrapper, + '\Wikimedia\Rdbms\IMaintainableDatabase::selectFieldValues' => $selectWrapper, '\Wikimedia\Rdbms\DBConnRef::selectFieldValues' => $selectWrapper, '\Wikimedia\Rdbms\Database::selectFieldValues' => $selectWrapper, - '\Wikimedia\Rdbms\IDatabase::selectSQLText' => $selectWrapper, - '\Wikimedia\Rdbms\DBConnRef::selectSQLText' => $selectWrapper, - '\Wikimedia\Rdbms\Database::selectSQLText' => $selectWrapper, + '\Wikimedia\Rdbms\IMaintainableDatabase::selectSQLText' => [ + 'overall' => self::YES_TAINT & ~self::SQL_TAINT + ] + $selectWrapper, + '\Wikimedia\Rdbms\IDatabase::selectSQLText' => [ + 'overall' => self::YES_TAINT & ~self::SQL_TAINT + ] + $selectWrapper, + '\Wikimedia\Rdbms\DBConnRef::selectSQLText' => [ + 'overall' => self::YES_TAINT & ~self::SQL_TAINT + ] + $selectWrapper, + '\Wikimedia\Rdbms\Database::selectSQLText' => [ + 'overall' => self::YES_TAINT & ~self::SQL_TAINT + ] + $selectWrapper, '\Wikimedia\Rdbms\IDatabase::selectRowCount' => $selectWrapper, + '\Wikimedia\Rdbms\IMaintainableDatabase::selectRowCount' => $selectWrapper, '\Wikimedia\Rdbms\Database::selectRowCount' => $selectWrapper, '\Wikimedia\Rdbms\DBConnRef::selectRowCount' => $selectWrapper, '\Wikimedia\Rdbms\IDatabase::selectRow' => $selectWrapper, + '\Wikimedia\Rdbms\IMaintainableDatabase::selectRow' => $selectWrapper, '\Wikimedia\Rdbms\Database::selectRow' => $selectWrapper, '\Wikimedia\Rdbms\DBConnRef::selectRow' => $selectWrapper, '\Wikimedia\Rdbms\IDatabase::delete' => [ + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + self::SQL_EXEC_TAINT, + 'overall' => self::NO_TAINT + ], + '\Wikimedia\Rdbms\IMaintainableDatabase::delete' => [ self::SQL_EXEC_TAINT, self::SQL_NUMKEY_EXEC_TAINT, self::SQL_EXEC_TAINT, @@ -130,6 +155,15 @@ 'overall' => self::NO_TAINT ], '\Wikimedia\Rdbms\IDatabase::insert' => [ + self::SQL_EXEC_TAINT, // table name + // FIXME This doesn't correctly work + // when inserting multiple things at once. + self::SQL_NUMKEY_EXEC_TAINT, + self::SQL_EXEC_TAINT, // method name + self::SQL_EXEC_TAINT, // options. They are not escaped + 'overall' => self::NO_TAINT + ], + '\Wikimedia\Rdbms\IMaintainableDatabase::insert' => [ self::SQL_EXEC_TAINT, // table name // FIXME This doesn't correctly work // when inserting multiple things at once. @@ -157,6 +191,14 @@ 'overall' => self::NO_TAINT ], '\Wikimedia\Rdbms\IDatabase::update' => [ + self::SQL_EXEC_TAINT, // table name + self::SQL_NUMKEY_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + self::SQL_EXEC_TAINT, // method name + self::NO_TAINT, // options. They are validated + 'overall' => self::NO_TAINT + ], + '\Wikimedia\Rdbms\IMaintainableDatabase::update' => [ self::SQL_EXEC_TAINT, // table name self::SQL_NUMKEY_EXEC_TAINT, self::SQL_NUMKEY_EXEC_TAINT, @@ -219,6 +261,10 @@ 'overall' => self::NO_TAINT, ], '\Wikimedia\Rdbms\IDatabase::addQuotes' => [ + self::YES_TAINT & ~self::SQL_TAINT, + 'overall' => self::NO_TAINT, + ], + '\Wikimedia\Rdbms\IMaintainableDatabase::addQuotes' => [ self::YES_TAINT & ~self::SQL_TAINT, 'overall' => self::NO_TAINT, ], @@ -331,6 +377,17 @@ SecurityCheckPlugin::NO_TAINT, 'overall' => SecurityCheckPlugin::NO_TAINT ], + '\Xml::encodeJsVar' => [ + self::YES_TAINT & ~self::HTML_TAINT, + self::NO_TAINT, /* pretty */ + 'overall' => self::NO_TAINT + ], + '\Xml::encodeJsCall' => [ + self::YES_TAINT, /* func name. unescaped */ + self::YES_TAINT & ~self::HTML_TAINT, /* args */ + self::NO_TAINT, /* pretty */ + 'overall' => self::NO_TAINT + ], '\OutputPage::addHeadItem' => [ SecurityCheckPlugin::HTML_EXEC_TAINT, 'overall' => self::NO_TAINT, @@ -443,6 +500,35 @@ 'CommentStore::insertWithTempTable' => [ 'overall' => self::NO_TAINT ], + // TODO FIXME, Why couldn't it figure out + // that this is safe on its own? + // It seems that it has issue with + // the url query parameters. + 'Linker::linkKnown' => [ + self::NO_TAINT, /* target */ + self::YES_TAINT, /* raw html text */ + // The array keys for this aren't escaped (!) + self::NO_TAINT, /* customAttribs */ + self::NO_TAINT, /* query */ + self::NO_TAINT, /* options. All are safe */ + 'overall' => self::NO_TAINT + ], + 'MediaWiki\Linker\LinkRenderer::buildAElement' => [ + self::NO_TAINT, /* target */ + self::NO_TAINT, /* text (using HtmlArmor) */ + // The array keys for this aren't escaped (!) + self::NO_TAINT, /* attribs */ + self::NO_TAINT, /* known */ + 'overall' => self::NO_TAINT + ], + 'MediaWiki\Linker\LinkRenderer::makeLink' => [ + self::NO_TAINT, /* target */ + self::NO_TAINT, /* text (using HtmlArmor) */ + // The array keys for this aren't escaped (!) + self::NO_TAINT, /* attribs */ + self::NO_TAINT, /* query */ + 'overall' => self::NO_TAINT + ], ]; } diff --git a/src/TaintednessBaseVisitor.php b/src/TaintednessBaseVisitor.php index 26acefa..c63003b 100644 --- a/src/TaintednessBaseVisitor.php +++ b/src/TaintednessBaseVisitor.php @@ -1529,7 +1529,7 @@ } elseif ( isset( $taint[$i] ) ) { if ( ( $taint[$i] & SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT ) - && !$this->nodeIsArray( $argument ) + && $this->nodeIsString( $argument ) && ( $curArgTaintedness & SecurityCheckPlugin::SQL_TAINT ) ) { // Special case to make NUMKEY work right for non-array @@ -1716,6 +1716,42 @@ } /** + * Given a Node, is it a string? (And definitely not an array) + * + * @todo Unclear if this should return true for things that can + * autocast to a string (e.g. ints) + * @param Mixed|Node $node A node object or simple value from AST tree + * @return bool Is it a string? + */ + protected function nodeIsString( $node ) : bool { + if ( is_string( $node ) ) { + return true; + } + if ( !( $node instanceof Node ) ) { + // simple literal + return false; + } + try { + $type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node + ); + if ( + !$type->hasArrayLike() && + $type->hasType( StringType::instance() ) + ) { + // FIXME TODO: Should having Mixed type + // result in returning false here? + return true; + } + } catch ( Exception $e ) { + $this->debug( __METHOD__, "Got error " . get_class( $e ) ); + } + return false; + } + + /** * Given a Node, is it definitely an int (and nothing else) * * Floats are not considered ints here. diff --git a/src/TaintednessVisitor.php b/src/TaintednessVisitor.php index db0b046..11cb08e 100644 --- a/src/TaintednessVisitor.php +++ b/src/TaintednessVisitor.php @@ -778,12 +778,12 @@ & SecurityCheckPlugin::SQL_NUMKEY_TAINT ) { $childTaint &= ~SecurityCheckPlugin::SQL_NUMKEY_TAINT; - - } elseif ( + } + if ( ( $this->getTaintedness( $key ) & $sqlTaint ) || ( ( $key === null || $this->nodeIsInt( $key ) ) && ( $this->getTaintedness( $value ) & $sqlTaint ) - && !$this->nodeIsArray( $value ) ) + && $this->nodeIsString( $value ) ) ) { $childTaint |= SecurityCheckPlugin::SQL_NUMKEY_TAINT; } @@ -888,6 +888,8 @@ // Don't include serialize here due to high false positives // Eventhough unserializing stuff from db can be very // problematic if user can ever control. + // FIXME This is MW specific so should not be + // in the generic visitor. return SecurityCheckPlugin::YES_TAINT & ~SecurityCheckPlugin::SERIALIZE_TAINT; } if ( diff --git a/tests/integration/arraynumkey/db.php b/tests/integration/arraynumkey/db.php index 7186cfe..25dd571 100644 --- a/tests/integration/arraynumkey/db.php +++ b/tests/integration/arraynumkey/db.php @@ -7,18 +7,40 @@ $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] ); + public function selectSQLText( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ); + public function selectRow( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ); } class Database implements IDatabase { public function query( $sql, $method ) { // do some stuff - return (object)[ 'some_field' => 'some value' ]; + return [ (object)[ 'some_field' => 'some value' ] ]; } public function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] ) { + return [ (object)[ 'some_field' => 'some value' ] ]; + } + + public function selectSQLText( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ) { + return 'SELECT * FROM foo'; + } + + public function selectRow( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ) { return (object)[ 'some_field' => 'some value' ]; } } diff --git a/tests/integration/arraynumkey/expectedResults.txt b/tests/integration/arraynumkey/expectedResults.txt index 949e140..4b38154 100644 --- a/tests/integration/arraynumkey/expectedResults.txt +++ b/tests/integration/arraynumkey/expectedResults.txt @@ -7,3 +7,4 @@ integration/arraynumkey/test.php:54 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $where5. (Caused by: integration/arraynumkey/test.php +53) integration/arraynumkey/test.php:59 SecurityCheck-SQLInjection The ON conditions are not properly escaped for the join to `t` (Caused by: integration/arraynumkey/test.php +53) (Originally at: integration/arraynumkey/test.php:57) integration/arraynumkey/test.php:80 SecurityCheck-SQLInjection HAVING clause is user controlled (Originally at: integration/arraynumkey/test.php:78) +integration/arraynumkey/test.php:109 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/arraynumkey/test.php +92) diff --git a/tests/integration/arraynumkey/test.php b/tests/integration/arraynumkey/test.php index e5dace8..f0dd600 100644 --- a/tests/integration/arraynumkey/test.php +++ b/tests/integration/arraynumkey/test.php @@ -83,3 +83,27 @@ $b = (int)$_GET['b']; $db->select( 't', 'f', [ 'foo' => $_GET['a'], "bar > $b" ] ); + +$row = (object)[ 'foo' => $_GET['bar'] ]; +$db->selectRow( 't', 'f', [ 'foo2' => $row->foo ] ); +$whereRow = [ 'foo2' => $row->foo ]; +$db->selectRow( 't', 'f', $whereRow ); + +$subquery = $db->selectSQLText( + 'Foo', + '1', + [ 'value' => $_GET['val'], 'baz=red', 'foo' => '<script>' ], + __METHOD__, + [ 'LIMIT' => 1 ] +); + +// safe +$db->select( + 'Bar', + '*', + [ 'NOT EXISTS( ' . $subquery . ')' ], + __METHOD__ +); + +// unsafe +echo $subquery; diff --git a/tests/integration/lists/test.php b/tests/integration/lists/test.php index c1bbbac..beb8b6d 100644 --- a/tests/integration/lists/test.php +++ b/tests/integration/lists/test.php @@ -6,3 +6,10 @@ list( $foo ) = getStuff(); echo $foo; + +function getSafeStuff() { + return [ 'a', 'b' ]; +} + +list( $a, $b ) = getSafeStuff(); +echo $b; diff --git a/tests/integration/lowercase/OutputPage.php b/tests/integration/lowercase/OutputPage.php new file mode 100644 index 0000000..9d54903 --- /dev/null +++ b/tests/integration/lowercase/OutputPage.php @@ -0,0 +1,5 @@ +<?php +class OutputPage { + public function addHTML( $html ) { + } +} diff --git a/tests/integration/lowercase/expectedResults.txt b/tests/integration/lowercase/expectedResults.txt new file mode 100644 index 0000000..d2a2014 --- /dev/null +++ b/tests/integration/lowercase/expectedResults.txt @@ -0,0 +1,4 @@ +integration/lowercase/test.php:5 SecurityCheck-XSS Calling method \OutputPage::addHTML() in [no method] that outputs using tainted argument $[arg #1]. +integration/lowercase/test.php:6 SecurityCheck-XSS Calling method \OutputPage::addHTML() in [no method] that outputs using tainted argument $[arg #1]. +integration/lowercase/test.php:7 SecurityCheck-XSS Calling method \OutputPage::addHTML() in [no method] that outputs using tainted argument $[arg #1]. +integration/lowercase/test.php:8 SecurityCheck-XSS Calling method \OutputPage::addHTML() in [no method] that outputs using tainted argument $[arg #1]. diff --git a/tests/integration/lowercase/test.php b/tests/integration/lowercase/test.php new file mode 100644 index 0000000..a510c65 --- /dev/null +++ b/tests/integration/lowercase/test.php @@ -0,0 +1,8 @@ +<?php + +$out = new OutputPage; + +$out->addHTML( $_GET['evil'] ); +$out->addHtml( $_GET['evil'] ); +$out->addhtml( $_GET['evil'] ); +$out->ADDHTML( $_GET['evil'] ); diff --git a/tests/integration/ooui/HorizontalLayout.php b/tests/integration/ooui/HorizontalLayout.php new file mode 100644 index 0000000..190955d --- /dev/null +++ b/tests/integration/ooui/HorizontalLayout.php @@ -0,0 +1,18 @@ +<?php +namespace OOUI; + +class Layout { + + protected $contents; + public function __toString() { + return $this->contents; + } + +} + +class HorizontalLayout extends Layout { + public function __construct( $contents ) { + $this->contents = $contents; + } + +} diff --git a/tests/integration/ooui/OutputPage.php b/tests/integration/ooui/OutputPage.php new file mode 100644 index 0000000..9d54903 --- /dev/null +++ b/tests/integration/ooui/OutputPage.php @@ -0,0 +1,5 @@ +<?php +class OutputPage { + public function addHTML( $html ) { + } +} diff --git a/tests/integration/ooui/expectedResults.txt b/tests/integration/ooui/expectedResults.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/integration/ooui/expectedResults.txt diff --git a/tests/integration/ooui/test.php b/tests/integration/ooui/test.php new file mode 100644 index 0000000..2b0577e --- /dev/null +++ b/tests/integration/ooui/test.php @@ -0,0 +1,9 @@ +<?php + +$out = new OutputPage; + +$stuff = new OOUI\HorizontalLayout( "More stuff" ); +$stuff2 = new OOUI\HorizontalLayout( $_GET['stuff'] ); + +$out->addHtml( "<span>" . $stuff . "</span>" ); +$out->addHtml( "<span>" . $stuff2 . "</span>" ); -- To view, visit https://gerrit.wikimedia.org/r/395762 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I3bcbd5bbfa7417c490adb2ec7b580e13eaa70741 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/tools/phan/SecurityCheckPlugin Gerrit-Branch: master Gerrit-Owner: Brian Wolff <bawolff...@gmail.com> Gerrit-Reviewer: Brian Wolff <bawolff...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits