Brian Wolff has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/394870 )
Change subject: Add support for IDatabase::select style arguments ...................................................................... Add support for IDatabase::select style arguments Change-Id: Id52f45e3dfc21a4c522902a8d4d92e61b40b7202 --- M src/MediaWikiSecurityCheckPlugin.php M src/TaintednessBaseVisitor.php M src/TaintednessVisitor.php A tests/integration/arraynumkey/db.php A tests/integration/arraynumkey/expectedResults.txt A tests/integration/arraynumkey/test.php A tests/integration/multiassign/expectedResults.txt A tests/integration/multiassign/test.php A tests/integration/multiassign/test2.php 9 files changed, 289 insertions(+), 1 deletion(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/tools/phan/SecurityCheckPlugin refs/changes/70/394870/1 diff --git a/src/MediaWikiSecurityCheckPlugin.php b/src/MediaWikiSecurityCheckPlugin.php index d17ebfa..2c25a07 100644 --- a/src/MediaWikiSecurityCheckPlugin.php +++ b/src/MediaWikiSecurityCheckPlugin.php @@ -76,6 +76,42 @@ // What should DB results be considered? 'overall' => self::YES_TAINT ], + '\Wikimedia\Rdbms\IDatabase::select' => [ + self::SQL_EXEC_TAINT, + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + // the function name doesn't seem to be escaped + self::SQL_EXEC_TAINT, + // I'm not even sure for options + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + // What should DB results be considered? + 'overall' => self::YES_TAINT + ], + '\Wikimedia\Rdbms\Database::select' => [ + self::SQL_EXEC_TAINT, + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + // the function name doesn't seem to be escaped + self::SQL_EXEC_TAINT, + // I'm not even sure for options + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + // What should DB results be considered? + 'overall' => self::YES_TAINT + ], + '\Wikimedia\Rdbms\DBConnRef::select' => [ + self::SQL_EXEC_TAINT, + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + // the function name doesn't seem to be escaped + self::SQL_EXEC_TAINT, + // I'm not even sure for options + self::SQL_EXEC_TAINT, + self::SQL_NUMKEY_EXEC_TAINT, + // What should DB results be considered? + 'overall' => self::YES_TAINT + ], '\Wikimedia\Rdbms\Database::addIdentifierQuotes' => [ self::YES_TAINT & ~self::SQL_TAINT, 'overall' => self::NO_TAINT, diff --git a/src/TaintednessBaseVisitor.php b/src/TaintednessBaseVisitor.php index 89a47b1..4a01337 100644 --- a/src/TaintednessBaseVisitor.php +++ b/src/TaintednessBaseVisitor.php @@ -12,6 +12,7 @@ use Phan\Language\UnionType; use Phan\Language\Type\CallableType; use Phan\Language\Type\MixedType; +use Phan\Language\Type\IntType; use Phan\Language\Type\StringType; use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName; use Phan\Language\FQSEN\FullyQualifiedFunctionName; @@ -1359,7 +1360,12 @@ ) { $issueType = 'SecurityCheck-XSS'; } elseif ( - $combinedTaint === SecurityCheckPlugin::SQL_TAINT + $combinedTaint === SecurityCheckPlugin::SQL_TAINT || + $combinedTaint === SecurityCheckPlugin::SQL_NUMKEY_TAINT || + $combinedTaint === ( + SecurityCheckPlugin::SQL_TAINT | + SecurityCheckPlugin::SQL_NUMKEY_TAINT + ) ) { $issueType = 'SecurityCheck-SQLInjection'; $severity = Issue::SEVERITY_CRITICAL; @@ -1459,6 +1465,16 @@ $curArgTaintedness = SecurityCheckPlugin::NO_TAINT; $effectiveArgTaintedness = SecurityCheckPlugin::NO_TAINT; } elseif ( isset( $taint[$i] ) ) { + if ( + ( $taint[$i] & SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT ) + && !$this->nodeIsArray( $argument ) + && ( $curArgTaintedness & SecurityCheckPlugin::SQL_TAINT ) + ) { + // Special case to make NUMKEY work right for non-array + // values. Should consider if this is really best + // approach. + $curArgTaintedness |= SecurityCheckPlugin::SQL_NUMKEY_TAINT; + } $effectiveArgTaintedness = $curArgTaintedness & ( $taint[$i] | $this->execToYesTaint( $taint[$i] ) ); // $this->debug( __METHOD__, "effective $effectiveArgTaintedness" @@ -1636,4 +1652,39 @@ } return false; } + + /** + * Given a Node, is it definitely an int (and nothing else) + * + * Floats are not considered ints here. + * + * @param Mixed|Node $node A node object or simple value from AST tree + * @return bool Is it an int? + */ + protected function nodeIsInt( $node ) : bool { + if ( is_int( $node ) ) { + return true; + } + if ( !( $node instanceof Node ) ) { + // simple literal that's not an int. + return false; + } + try { + $type = UnionTypeVisitor::unionTypeFromNode( + $this->code_base, + $this->context, + $node + ); + if ( + $type->hasType( IntType::instance() ) && + $type->typeCount() === 1 + ) { + return true; + } + } catch ( Exception $e ) { + $this->debug( __METHOD__, "Got error " . get_class( $e ) ); + } + return false; + } + } diff --git a/src/TaintednessVisitor.php b/src/TaintednessVisitor.php index d4b588d..2d45a58 100644 --- a/src/TaintednessVisitor.php +++ b/src/TaintednessVisitor.php @@ -287,6 +287,7 @@ /** * Also handles visitAssignOp * + * @todo FIXME, doesn't handle list( $a, $b ) = $foo; * @param Node $node * @return int Taint */ @@ -319,6 +320,26 @@ // TODO, be more specific for different OPs // Expand rhs to include implicit lhs ophand. $rhsTaintedness = $this->mergeAddTaint( $rhsTaintedness, $lhsTaintedness ); + } + + // Special case for SQL_NUMKEY_TAINT + // If we're assigning an SQL tainted value as an array key + // or as the value of a numeric key, then set NUMKEY taint. + $var = $node->children['var']; + if ( $var->kind === \ast\AST_DIM ) { + $dim = $var->children['dim']; + if ( $rhsTaintedness & SecurityCheckPlugin::SQL_TAINT ) { + if ( + $dim === null || + $this->nodeIsInt( $dim ) + ) { + $rhsTaintedness |= SecurityCheckPlugin::SQL_NUMKEY_TAINT; + } + } + if ( $this->getTaintedness( $dim ) & SecurityCheckPlugin::SQL_TAINT ) { + $rhsTaintedness |= SecurityCheckPlugin::SQL_NUMKEY_TAINT; + + } } // If we're assigning to a variable we know will be output later @@ -739,6 +760,16 @@ foreach ( $node->children as $child ) { assert( $child->kind === \ast\AST_ARRAY_ELEM ); $curTaint = $this->mergeAddTaint( $curTaint, $this->getTaintedness( $child ) ); + $key = $child->children['key']; + $value = $child->children['value']; + $sqlTaint = SecurityCheckPlugin::SQL_TAINT; + if ( + ( $this->getTaintedness( $key ) & $sqlTaint ) || + ( ( $key === null || $this->nodeIsInt( $key ) ) + && ( $this->getTaintedness( $value ) & $sqlTaint ) ) + ) { + $curTaint |= SecurityCheckPlugin::SQL_NUMKEY_TAINT; + } } return $curTaint; } diff --git a/tests/integration/arraynumkey/db.php b/tests/integration/arraynumkey/db.php new file mode 100644 index 0000000..c607198 --- /dev/null +++ b/tests/integration/arraynumkey/db.php @@ -0,0 +1,26 @@ +<?php +namespace Wikimedia\Rdbms; + +interface IDatabase { + public function query( $sql, $method ); +} + +class Database implements IDatabase { + public function query( $sql, $method ) { + // do some stuff + return (object)[ 'some_field' => 'some value' ]; + } + + public function select( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ) { + return (object)[ 'some_field' => 'some value' ]; + } +} + +class MysqlDatabase extends Database { + public function getType() { + return 'mysql'; + } +} diff --git a/tests/integration/arraynumkey/expectedResults.txt b/tests/integration/arraynumkey/expectedResults.txt new file mode 100644 index 0000000..ec727f1 --- /dev/null +++ b/tests/integration/arraynumkey/expectedResults.txt @@ -0,0 +1,7 @@ +integration/arraynumkey/test.php:15 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $[arg #3]. +integration/arraynumkey/test.php:22 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $[arg #3]. +integration/arraynumkey/test.php:32 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $where. (Caused by: integration/arraynumkey/test.php +29) +integration/arraynumkey/test.php:41 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $where2. (Caused by: integration/arraynumkey/test.php +38) +integration/arraynumkey/test.php:45 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $where3. (Caused by: integration/arraynumkey/test.php +44) +integration/arraynumkey/test.php:50 SecurityCheck-SQLInjection Calling method \Wikimedia\Rdbms\MysqlDatabase::select() in [no method] that outputs using tainted argument $where4. +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) diff --git a/tests/integration/arraynumkey/test.php b/tests/integration/arraynumkey/test.php new file mode 100644 index 0000000..ca63c55 --- /dev/null +++ b/tests/integration/arraynumkey/test.php @@ -0,0 +1,54 @@ +<?php + +use Wikimedia\Rdbms\MysqlDatabase; + +$db = new MysqlDatabase; + +// safe +$db->select( + 'table', + 'field', + [ 'foo' => $_GET['bar'] ] +); + +// unsafe +$db->select( + 'table', + 'field', + [ $_GET['bar'] ] +); + +// unsafe +$db->select( + 'table', + 'field', + $_GET['bar'] +); + +$where = []; +$where[] = $_GET['bar']; + +// unsafe +$db->select( + 'table', + 'field', + $where +); + +$where2 = [ $_GET['bar'] ]; +$where2[] = 'Something'; +// unsafe +$db->select( 't', 'f', $where2 ); + +// unsafe +$where3 = [ $_GET['d'] => 'foo' ]; +$db->select( 't', 'f', $where3 ); + +$where4 = []; +$where4[$_GET['d']] = 'foo'; +// unsafe +$db->select( 't', 'f', $where4 ); + +// unsafe +$where5 = [ 1 => $_GET['d'] ]; +$db->select( 't', 'f', $where5 ); diff --git a/tests/integration/multiassign/expectedResults.txt b/tests/integration/multiassign/expectedResults.txt new file mode 100644 index 0000000..1f2f885 --- /dev/null +++ b/tests/integration/multiassign/expectedResults.txt @@ -0,0 +1,5 @@ +integration/multiassign/test.php:16 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/multiassign/test.php +12) +integration/multiassign/test2.php:20 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/multiassign/test2.php +12) +integration/multiassign/test2.php:39 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/multiassign/test2.php +12) +integration/multiassign/test2.php:41 SecurityCheck-XSS Echoing expression that was not html escaped +integration/multiassign/test2.php:54 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/multiassign/test2.php +51) diff --git a/tests/integration/multiassign/test.php b/tests/integration/multiassign/test.php new file mode 100644 index 0000000..5e6e2f1 --- /dev/null +++ b/tests/integration/multiassign/test.php @@ -0,0 +1,17 @@ +<?php + +function test() { + $a = $_GET['d']; + + $a = 'ok'; + + echo $a; +} + +function test2() { + $b = $_GET['d']; + if ( false ) { + $b = 'ok'; + } + echo $b; +} diff --git a/tests/integration/multiassign/test2.php b/tests/integration/multiassign/test2.php new file mode 100644 index 0000000..f0c1d8e --- /dev/null +++ b/tests/integration/multiassign/test2.php @@ -0,0 +1,61 @@ +<?php + +$globalFoo = ''; +$globalBar = ''; + +class Foo { + private $bar; + private $bar2; + + public function setEvil() { + global $globalFoo; + $this->bar2 = $_GET['evil']; + $globalFoo = $_GET['evil']; + // Note: intentional lack of global decleration. + $globalBar = $_GET['evil']; + } + + public function echoThing() { + // Should give warning + echo $this->bar2; + } + + public function setGood() { + global $globalFoo, $globalBar; + $this->bar2 = 'good'; + $globalFoo = 'good'; + $globalBar = 'good'; + } + + /** + * This should trigger a warning, since + * there is no garuntee that setGood() + * was called before this despite it being + * earlier in the source file + */ + public function echoThing() { + global $globalFoo, $globalBar; + // Should give warning + echo $this->bar2; + // Should give warning + echo $globalFoo; + // No warning + echo $globalBar; + } + + public function f1() { + // At the moment, this triggers a warning + // Because we are not smart enough to know + // if any non-local functions have modified + // $this->bar in between setting it to something good. + $this->bar = $_GET['tainted']; + $this->bar = 'ok'; + // Should give warning + echo $this->bar; + } +} + +$f = new Foo; + +$f->setEvil(); +$f->echoThing(); -- To view, visit https://gerrit.wikimedia.org/r/394870 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Id52f45e3dfc21a4c522902a8d4d92e61b40b7202 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/tools/phan/SecurityCheckPlugin Gerrit-Branch: master Gerrit-Owner: Brian Wolff <bawolff...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits