Brian Wolff has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/393540 )
Change subject: Support ParserFunctions, and start of work for hooks in general
......................................................................
Support ParserFunctions, and start of work for hooks in general
Also fix bug sometimes function taint would get lost if
setting a different taint for another parameter
Add -v option to the test runner
Change-Id: Idcd74eee6ad5fb38d7c8b81f4cf1058dc26f080b
---
M .phpcs.xml
A src/MWPreVisitor.php
A src/MWVisitor.php
M src/MediaWikiSecurityCheckPlugin.php
M src/TaintednessBaseVisitor.php
M src/TaintednessVisitor.php
A tests/integration/callbackhook/Parser.php
A tests/integration/callbackhook/db.php
A tests/integration/callbackhook/expectedResults.txt
A tests/integration/callbackhook/func.php
A tests/integration/callbackhook/test.php
A tests/integration/stripitem/Parser.php
A tests/integration/stripitem/expectedResults.txt
A tests/integration/stripitem/test.php
M tests/runtests.sh
15 files changed, 833 insertions(+), 17 deletions(-)
diff --git a/.phpcs.xml b/.phpcs.xml
index 60518ae..8b2fe7e 100644
--- a/.phpcs.xml
+++ b/.phpcs.xml
@@ -4,6 +4,7 @@
<exclude
name="MediaWiki.NamingConventions.PrefixedGlobalFunctions.wfPrefix"/>
<exclude name="MediaWiki.Usage.ScalarTypeHintUsage.Found"/>
<exclude
name="MediaWiki.Commenting.FunctionComment.MissingParamComment"/>
+ <exclude
name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment"/>
</rule>
<file>.</file>
<arg name="bootstrap"
value="./vendor/mediawiki/mediawiki-codesniffer/utils/bootstrap-ci.php"/>
@@ -23,6 +24,12 @@
<rule
ref="MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic">
<exclude-pattern>./tests</exclude-pattern>
</rule>
+ <rule
ref="MediaWiki.Commenting.FunctionComment.MissingDocumentationProtected">
+ <exclude-pattern>./tests</exclude-pattern>
+ </rule>
+ <rule
ref="MediaWiki.Commenting.FunctionComment.MissingDocumentationPrivate">
+ <exclude-pattern>./tests</exclude-pattern>
+ </rule>
<rule ref="MediaWiki.Files.ClassMatchesFilename.NotMatch">
<exclude-pattern>./tests</exclude-pattern>
</rule>
diff --git a/src/MWPreVisitor.php b/src/MWPreVisitor.php
new file mode 100644
index 0000000..ebe5042
--- /dev/null
+++ b/src/MWPreVisitor.php
@@ -0,0 +1,119 @@
+<?php
+
+use Phan\Language\Context;
+use ast\Node;
+use ast\Node\Decl;
+use Phan\Debug;
+use Phan\CodeBase;
+use Phan\Language\Element\FunctionInterface;
+
+/**
+ * Class for visiting any nodes we want to handle in pre-order.
+ *
+ */
+class MWPreVisitor extends TaintednessBaseVisitor {
+
+
+ /**
+ * Ensure type of plugin is instance of MediaWikiSecurityCheckPlugin
+ *
+ * @param CodeBase $code_base
+ * @param Context $context
+ * @param MediaWikiSecurityCheckPlugin
+ */
+ public function __construct(
+ CodeBase $code_base,
+ Context $context,
+ MediaWikiSecurityCheckPlugin $plugin
+ ) {
+ parent::__construct( $code_base, $context, $plugin );
+ // Ensure phan knows plugin is right type
+ $this->plugin = $plugin;
+ }
+
+ /**
+ * Handle any node not otherwise handled.
+ *
+ * Currently a no-op.
+ *
+ * @param Node $node
+ */
+ public function visit( Node $node ) {
+ }
+
+ /**
+ * @see visitMethod
+ * @param Decl $node
+ * @return void Just has a return statement in case visitMethod changes
+ */
+ public function visitFuncDecl( Decl $node ) {
+ return $this->visitMethod( $node );
+ }
+
+ /**
+ * Set taint for certain hook types.
+ *
+ * Also handles FuncDecl
+ * @param Decl $node
+ */
+ public function visitMethod( Decl $node ) {
+ $method = $this->context->getFunctionLikeInScope(
$this->code_base );
+ $hookType = $this->plugin->isSpecialHookSubscriber(
$method->getFQSEN() );
+ if ( !$hookType ) {
+ return;
+ }
+ $params = $node->children['params']->children;
+
+ switch ( $hookType ) {
+ case '!ParserFunctionHook':
+ $this->setFuncHookParamTaint( $params, $method );
+ break;
+ }
+ }
+
+ /**
+ * Set the appropriate taint for a parser function hook
+ *
+ * Basically all but the first arg comes from wikitext
+ * and is tainted.
+ *
+ * @todo This is handling SFH_OBJECT type func hooks incorrectly.
+ * @param Node[] $params Children of the AST_PARAM_LIST
+ * @param FunctionInterface $method
+ */
+ private function setFuncHookParamTaint( array $params,
FunctionInterface $method ) {
+ $this->debug( __METHOD__, "Setting taint for hook $method" );
+ $funcTaint = $this->getTaintOfFunction( $method );
+ $varObjs = [];
+ foreach ( $params as $i => $param ) {
+ if ( $i === 0 ) {
+ continue;
+ }
+ $scope = $this->context->getScope();
+ if ( !$scope->hasVariableWithName(
$param->children['name'] ) ) {
+ // Well uh-oh.
+ $this->debug( __METHOD__, "Missing variable for
param \$" . $param->children['name'] );
+ continue;
+ }
+ $varObj = $scope->getVariableByName(
$param->children['name'] );
+ $this->setTaintedness( $varObj,
SecurityCheckPlugin::YES_TAINT );
+ /*** Is this needed ? Disabling for now.
+ if ( isset( $funcTaint[$i] ) ) {
+ if ( !$this->isSafeAssignment(
+ $funcTaint[$i],
+ SecurityCheckPlugin::YES_TAINT
+ ) ) {
+ $funcName = $method->getFQSEN();
+ $this->plugin->emitIssue(
+ $this->code_base,
+ $this->context,
+ 'SecurityCheckTaintedOutput',
+ "Outputting evil HTML from
Parser function hook $funcName (case 2)"
+ .
$this->getOriginalTaintLine( $method )
+ );
+ }
+ }
+ */
+ }
+ }
+}
diff --git a/src/MWVisitor.php b/src/MWVisitor.php
new file mode 100644
index 0000000..76a9909
--- /dev/null
+++ b/src/MWVisitor.php
@@ -0,0 +1,169 @@
+<?php
+
+use Phan\Language\Context;
+use Phan\Language\FQSEN\FullyQualifiedMethodName;
+use Phan\Language\FQSEN\FullyQualifiedFunctionName;
+use Phan\Language\FQSEN;
+use Phan\Plugin;
+use Phan\CodeBase;
+use ast\Node;
+
+/**
+ * MediaWiki specific node visitor
+ */
+class MWVisitor extends TaintednessBaseVisitor {
+
+ /**
+ * Constructor to enforce plugin is instance of
MediaWikiSecurityCheckPlugin
+ *
+ * @param CodeBase $code_base
+ * @param Context $context
+ * @param MediaWikiSecurityCheckPlugin $plugin
+ */
+ public function __construct(
+ CodeBase $code_base,
+ Context $context,
+ MediaWikiSecurityCheckPlugin $plugin
+ ) {
+ parent::__construct( $code_base, $context, $plugin );
+ // Ensure phan knows plugin is right type
+ $this->plugin = $plugin;
+ }
+
+ /**
+ * @param Node $node
+ */
+ public function visit( Node $node ) {
+ }
+
+ /**
+ * Try and recognize
+ * @param Node $node
+ */
+ public function visitMethodCall( Node $node ) {
+ try {
+ $ctx = $this->getCtxN( $node );
+ $methodName = $node->children['method'];
+ $method = $ctx->getMethod(
+ $methodName,
+ false /* not a static call */
+ );
+ // Should this be getDefiningFQSEN() instead?
+ switch ( (string)$method->getFQSEN() ) {
+ case '\Parser::setFunctionHook':
+ $this->handleFuncRegistration( $node );
+ break;
+ }
+ } catch ( Exception $e ) {
+ // ignore
+ }
+ }
+
+ /**
+ * When someone calls $parser->setFunctionHook()
+ *
+ * @note Causes phan to error out if given non-existent class
+ * @param Node $node
+ */
+ private function handleFuncRegistration( Node $node ) {
+ $args = $node->children['args']->children;
+ if ( count( $args ) < 2 ) {
+ return;
+ }
+ $callback = $this->getFQSENFromCallable( $args[1] );
+ if ( $callback ) {
+ $alreadyRegistered = $this->plugin->registerHook(
'!ParserFunctionHook', $callback );
+ if ( !$alreadyRegistered ) {
+ // If this is the first time seeing this,
re-analyze the
+ // node, just in case we had already passed it
by.
+ 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 );
+ } elseif ( $callback instanceof
FullyQualifiedMethodName ) {
+ $func =
$this->code_base->getMethodByFQSEN( $callback );
+ } else {
+ assert( $callback instanceof
FullyQualifiedFunctionName );
+ $func =
$this->code_base->getFunctionByFQSEN( $callback );
+ }
+ $func->analyze(
+ $func->getContext(),
+ $this->code_base
+ );
+ }
+ }
+ }
+
+ /**
+ * For special hooks, check their return value
+ *
+ * e.g. A tag hook's return value is output as html.
+ * @param Node $node
+ */
+ public function visitReturn( Node $node ) {
+ if (
+ !$this->context->isInFunctionLikeScope()
+ || !$node->children['expr'] instanceof Node
+ ) {
+ return;
+ }
+ $funcFQSEN = $this->context->getFunctionLikeFQSEN();
+ $hookType = $this->plugin->isSpecialHookSubscriber( $funcFQSEN
);
+ switch ( $hookType ) {
+ case '!ParserFunctionHook':
+ $this->visitReturnOfFunctionHook(
$node->children['expr'], $funcFQSEN );
+ break;
+
+ }
+ }
+
+ /**
+ * Check to see if isHTML => true and is tainted.
+ *
+ * @param Node $node The expr child of the return. NOT the return itself
+ * @suppress PhanTypeMismatchForeach
+ */
+ private function visitReturnOfFunctionHook( Node $node, FQSEN $funcName
) {
+ if (
+ !( $node instanceof Node ) ||
+ $node->kind !== \ast\AST_ARRAY ||
+ count( $node->children ) < 2
+ ) {
+ return;
+ }
+ $isHTML = false;
+ foreach ( $node->children as $child ) {
+ assert(
+ $child instanceof Node
+ && $child->kind === \ast\AST_ARRAY_ELEM
+ );
+
+ if (
+ $child->children['key'] === 'isHTML' &&
+ $child->children['value'] instanceof Node &&
+ $child->children['value']->kind ===
\ast\AST_CONST &&
+ $child->children['value']->children['name']
instanceof Node &&
+
$child->children['value']->children['name']->children['name'] === 'true'
+ ) {
+ $isHTML = true;
+ break;
+ }
+ }
+ if ( !$isHTML ) {
+ return;
+ }
+ $taintedness = $this->getTaintedness( $node->children[0] );
+ if ( !$this->isSafeAssignment(
SecurityCheckPlugin::HTML_EXEC_TAINT, $taintedness ) ) {
+ $this->plugin->emitIssue(
+ $this->code_base,
+ $this->context,
+ 'SecurityCheckTaintedOutput',
+ "Outputting evil HTML from Parser function hook
$funcName"
+ . $this->getOriginalTaintLine(
$node->children[0] )
+ );
+ }
+ }
+}
diff --git a/src/MediaWikiSecurityCheckPlugin.php
b/src/MediaWikiSecurityCheckPlugin.php
index 232ecfe..e621b94 100644
--- a/src/MediaWikiSecurityCheckPlugin.php
+++ b/src/MediaWikiSecurityCheckPlugin.php
@@ -3,8 +3,52 @@
* Copyright Brian Wolff 2017. Released under the GPL version 2 or later.
*/
require_once __DIR__ . "/SecurityCheckPlugin.php";
+require_once __DIR__ . "/MWVisitor.php";
+require_once __DIR__ . "/MWPreVisitor.php";
+
+use Phan\CodeBase;
+use Phan\Language\Context;
+use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName;
+use ast\Node;
class MediaWikiSecurityCheckPlugin extends SecurityCheckPlugin {
+
+ /**
+ * @var Array A mapping from hook names to FQSEN that implement it
+ */
+ protected $hookSubscribers = [];
+
+ /**
+ * Override so we can check for hook registration
+ *
+ * @param CodeBase $code_base
+ * @param Context $context
+ * @param Node $node
+ * @param Node $parentNode
+ */
+ public function analyzeNode(
+ CodeBase $code_base,
+ Context $context,
+ Node $node,
+ Node $parentNode = null
+ ) {
+ parent::analyzeNode( $code_base, $context, $node, $parentNode );
+
+ $visitor = new MWVisitor( $code_base, $context, $this );
+ $visitor( $node );
+ }
+
+ /**
+ * Called on every node in the ast, but in pre-order
+ *
+ * @param CodeBase $code_base
+ * @param Context $context
+ * @param Node $node
+ */
+ public function preAnalyzeNode( CodeBase $code_base, Context $context,
Node $node ) {
+ parent::preAnalyzeNode( $code_base, $context, $node );
+ ( new MWPreVisitor( $code_base, $context, $this ) )( $node );
+ }
/**
* @inheritDoc
@@ -76,6 +120,13 @@
SecurityCheckPlugin::HTML_EXEC_TAINT,
// meh, not sure how right the overall is.
'overall' => SecurityCheckPlugin::HTML_TAINT
+ ],
+ // AddItem should also take care of addGeneral and
friends.
+ '\StripState::addItem' => [
+ self::NO_TAINT, // type
+ self::NO_TAINT, // marker
+ self::HTML_EXEC_TAINT, // contents
+ 'overall' => self::NO_TAINT
],
// FIXME Doesn't handle array args right.
'\wfShellExec' => [
@@ -188,6 +239,68 @@
];
}
+ /**
+ * Add a hook implementation to our list.
+ *
+ * This also handles parser hooks which aren't normal hooks.
+ * Non-normal hooks start their name with a "!"
+ *
+ * @param string $hookName Name of hook
+ * @param FullyQualifiedFunctionLikeName $fqsen The implementing method
+ * @return bool true if already registered, false otherwise
+ */
+ public function registerHook( string $hookName,
FullyQualifiedFunctionLikeName $fqsen ) {
+ if ( !isset( $this->hookSubscribers[$hookName] ) ) {
+ $this->hookSubscribers[$hookName] = [];
+ }
+ foreach ( $this->hookSubscribers[$hookName] as $subscribe ) {
+ if ( (string)$subscribe === (string)$fqsen ) {
+ // dupe
+ return true;
+ }
+ }
+ $this->hookSubscribers[$hookName][] = $fqsen;
+ return false;
+ }
+
+ /**
+ * Get a list of subscribers for hook
+ *
+ * @param string $hookName Hook in question. Hooks starting with ! are
special.
+ * @return FullyQualifiedFunctionLikeName[]
+ */
+ public function getHookSubscribers( string $hookName ) : array {
+ if ( isset( $this->hookSubscribers[$hookName] ) ) {
+ return $this->hookSubscribers[$hookName];
+ }
+ return [];
+ }
+
+ /**
+ * Is a particular function implementing a special hook.
+ *
+ * @note This assumes that any given func will only implement
+ * one hook
+ * @param FullyQualifiedFunctionLikeName $fqsen The function to check
+ * @return string The hook it is implementing
+ */
+ public function isSpecialHookSubscriber( FullyQualifiedFunctionLikeName
$fqsen ) {
+ $specialHooks = [
+ '!ParserFunctionHook'
+ ];
+
+ // @todo This is probably not the most efficient thing.
+ foreach ( $specialHooks as $hook ) {
+ if ( !isset( $this->hookSubscribers[$hook] ) ) {
+ continue;
+ }
+ foreach ( $this->hookSubscribers[$hook] as $implFQSEN )
{
+ if ( (string)$implFQSEN === (string)$fqsen ) {
+ return $hook;
+ }
+ }
+ }
+ }
}
return new MediaWikiSecurityCheckPlugin;
diff --git a/src/TaintednessBaseVisitor.php b/src/TaintednessBaseVisitor.php
index 58195b7..52fb30d 100644
--- a/src/TaintednessBaseVisitor.php
+++ b/src/TaintednessBaseVisitor.php
@@ -9,7 +9,10 @@
use Phan\Language\Element\TypedElementInterface;
use Phan\Language\Element\ClassElement;
use Phan\Language\UnionType;
+use Phan\Language\Type\CallableType;
use Phan\Language\FQSEN\FullyQualifiedFunctionLikeName;
+use Phan\Language\FQSEN\FullyQualifiedFunctionName;
+use Phan\Language\FQSEN\FullyQualifiedMethodName;
use Phan\Plugin;
use ast\Node;
use Phan\Debug;
@@ -74,7 +77,10 @@
if ( $override ) {
$newTaint = $taint;
}
- foreach ( $taint as $index => $t ) {
+
+ $bothTaint = $taint + $curTaint;
+ foreach ( $bothTaint as $index => $_ ) {
+ $t = $taint[$index] ?? 0;
assert( is_int( $t ) );
if ( !$override ) {
$newTaint[$index] = ( $curTaint[$index] ?? 0 )
| $t;
@@ -192,7 +198,7 @@
$parentVarObj->taintedOriginalError =&
$variableObj->taintedOriginalError;
} else {
- $this->debug( __METHOD__, "var
{$variableObj->getName()} does not exist outside branch!" );
+ // $this->debug( __METHOD__, "var
{$variableObj->getName()} does not exist outside branch!" );
}
}
// This may not be executed, so it can only increase
@@ -825,7 +831,7 @@
$paramTaint = [ 'overall' =>
SecurityCheckPlugin::NO_TAINT ];
foreach ( $paramInfo as $i => $_ ) {
$paramTaint[$i] = $taint;
- // $this->debug( __METHOD__ , "Setting method
$method" .
+ // $this->debug( __METHOD__, "Setting method
$method" .
// " arg $i as $taint due to depenency
on $var" );
}
$this->setFuncTaint( $method, $paramTaint );
@@ -1114,4 +1120,161 @@
assert( is_int( $t ) && $t >= 0, "Taint index $i wrong
$t" . $this->dbgInfo() );
}
}
+
+ /**
+ * Given an AST node that's a callable, try and determine what it is
+ *
+ * This is intended for functions that register callbacks. It will
+ * only really work for callbacks that are basically literals.
+ *
+ * @note $node may not be the current node in $this->context.
+ *
+ * @param Node|string $node The thingy from AST expected to be a
Callable
+ * @return FullyQualifiedFunctionLikeName|null The corresponding FQSEN
+ */
+ protected function getFQSENFromCallable( $node ) {
+ $callback = null;
+ if ( is_string( $node ) ) {
+ // Easy case, 'Foo::Bar'
+ if ( strpos( '::', $callback ) === false ) {
+ $callback =
FullyQualifiedFunctionName::fromFullyQualifiedString(
+ $node
+ );
+ } else {
+ $callback =
FullyQualifiedMethodName::fromFullyQualifiedString(
+ $node
+ );
+ }
+ } elseif ( $node instanceof Node && $node->kind ===
\ast\AST_CLOSURE ) {
+ $method = (
+ new ContextNode(
+ $this->code_base,
+ $this->context->withLineNumberStart(
+ $node->lineno ?? 0
+ ),
+ $node
+ )
+ )->getClosure();
+ $callback = $method->getFQSEN();
+ } elseif (
+ $node instanceof Node
+ && $node->kind === \ast\AST_VAR
+ && is_string( $node->children['name'] )
+ ) {
+ $cnode = $this->getCtxN( $node );
+ $var = $cnode->getVariable();
+ $types = $var->getUnionType()->getTypeSet();
+ foreach ( $types as $type ) {
+ if (
+ $type instanceof CallableType &&
+ $type->asFQSEN() instanceof
FullyQualifiedFunctionLikeName
+ ) {
+ // @todo FIXME This doesn't work if the
closure
+ // is defined in a different function
scope
+ // then the one we are currently in.
Perhaps
+ // we could look up the closure in
+ // $this->code_base to figure out what
func
+ // its defined on via its parent scope.
Or
+ // something.
+ $callback = $type->asFQSEN();
+ break;
+ }
+ }
+ } elseif ( $node instanceof Node && $node->kind ===
\ast\AST_ARRAY ) {
+ if ( count( $node->children ) !== 2 ) {
+ return null;
+ }
+ if (
+ $node->children[0]->children['key'] !== null ||
+ $node->children[1]->children['key'] !== null ||
+ !is_string(
$node->children[1]->children['value'] )
+ ) {
+ return null;
+ }
+ $methodName = $node->children[1]->children['value'];
+ $classNode = $node->children[0]->children['value'];
+ if ( is_string( $node->children[0]->children['value'] )
) {
+ $className = $classNode;
+ } elseif ( $classNode instanceof Node ) {
+ switch ( $classNode->kind ) {
+ case \ast\AST_MAGIC_CONST:
+ // Mostly a special case for MediaWiki
+ // CoreParserFunctions.php
+ if (
+ $classNode->flags &
\ast\flags\MAGIC_CLASS !== 0
+ &&
$this->context->isInClassScope()
+ ) {
+ $className =
(string)$this->context->getClassFQSEN();
+ } else {
+ return null;
+ }
+ break;
+ case \ast\AST_CLASS_CONST:
+ if (
+ $classNode->children['const']
=== 'class' &&
+
$classNode->children['class']->kind === \ast\AST_NAME &&
+ is_string(
$classNode->children['class']->children['name'] )
+ ) {
+ $className =
$classNode->children['class']->children['name'];
+ } else {
+ return null;
+ }
+ break;
+ case \ast\AST_VAR:
+ $var = $this->getCtxN( $classNode
)->getVariable();
+ $type = $var->getUnionType();
+ if ( $type->typeCount() !== 1 ||
$type->isScalar() ) {
+ return null;
+ }
+ $cl = $type->asClassList(
+ $this->code_base,
+ $this->context
+ );
+ $clazz = false;
+ foreach ( $cl as $item ) {
+ $clazz = $item;
+ break;
+ }
+ if ( !$clazz ) {
+ return null;
+ }
+ $className = (string)$clazz->getFQSEN();
+ break;
+ case \ast\AST_PROP:
+ $var = $this->getCtxN( $classNode )
+ ->getProperty(
$classNode->children['prop'] );
+ $type = $var->getUnionType();
+ if ( $type->typeCount() !== 1 ||
$type->isScalar() ) {
+ return null;
+ }
+ $cl = $type->asClassList(
+ $this->code_base,
+ $this->context
+ );
+ $clazz = false;
+ foreach ( $cl as $item ) {
+ $clazz = $item;
+ break;
+ }
+ if ( !$clazz ) {
+ return null;
+ }
+ $className = (string)$clazz->getFQSEN();
+ break;
+ default:
+ return null;
+ }
+
+ } else {
+ return null;
+ }
+ // Note, not from in context, since this goes to
call_user_func.
+ $callback =
FullyQualifiedMethodName::fromFullyQualifiedString(
+ $className . '::' . $methodName
+ );
+ } else {
+ return null;
+ }
+ return $callback;
+ }
}
diff --git a/src/TaintednessVisitor.php b/src/TaintednessVisitor.php
index aa4b54c..7a45000 100644
--- a/src/TaintednessVisitor.php
+++ b/src/TaintednessVisitor.php
@@ -104,6 +104,22 @@
* @param Node $node
* @return int Taint
*/
+ public function visitUseElem( Node $node ) : int {
+ return SecurityCheckPlugin::INAPLICABLE_TAINT;
+ }
+
+ /**
+ * @param Node $node
+ * @return int Taint
+ */
+ public function visitType( Node $node ) : int {
+ return SecurityCheckPlugin::INAPLICABLE_TAINT;
+ }
+
+ /**
+ * @param Node $node
+ * @return int Taint
+ */
public function visitArgList( Node $node ) : int {
return SecurityCheckPlugin::INAPLICABLE_TAINT;
}
@@ -546,8 +562,8 @@
if ( isset( $taint[$i] ) ) {
$effectiveArgTaintedness = $curArgTaintedness &
( $taint[$i] | $this->execToYesTaint(
$taint[$i] ) );
- # $this->debug( __METHOD__, "effective
$effectiveArgTaintedness"
- # . " via arg $i $funcName" );
+ // $this->debug( __METHOD__, "effective
$effectiveArgTaintedness"
+ // . " via arg $i $funcName" );
} elseif ( ( $taint['overall'] &
( SecurityCheckPlugin::PRESERVE_TAINT |
SecurityCheckPlugin::UNKNOWN_TAINT )
) ) {
@@ -621,8 +637,8 @@
// someFunc( $execArg ) for pass by reference where
// the parameter is later executed outside the func.
if ( $func && $this->isYesTaint( $curArgTaintedness ) )
{
- # $this->debug( __METHOD__, "cur arg $i is YES
taint " .
- # "($curArgTaintedness). Marking dependent
$funcName" );
+ // $this->debug( __METHOD__, "cur arg $i is YES
taint " .
+ // "($curArgTaintedness). Marking dependent
$funcName" );
// Mark all dependent vars as tainted.
$this->markAllDependentVarsYes( $func, $i );
}
@@ -631,7 +647,7 @@
// where $arg is a parameter to the current function.
// So backpropagate that assigning to $arg can cause
evilness.
if ( $this->isExecTaint( $taint[$i] ?? 0 ) ) {
- # $this->debug( __METHOD__, "cur param is EXEC.
$funcName" );
+ // $this->debug( __METHOD__, "cur param is
EXEC. $funcName" );
try {
$phanObjs = $this->getPhanObjsForNode(
$argument );
foreach ( $phanObjs as $phanObj ) {
@@ -646,7 +662,7 @@
// gets passed to return value, but which taint is
EXECed.
// $this->debug( __METHOD__, "Checking safe assing
$funcName" .
// " arg=$i paramTaint= " . ( $taint[$i] ??
"MISSING" ) .
- // " vs argTaint= $curArgTaintedness" );
+ // " vs argTaint= $curArgTaintedness" );
if ( !$this->isSafeAssignment( $taint[$i] ?? 0,
$curArgTaintedness ) ) {
$containingMethod = $this->getCurrentMethod();
$this->plugin->emitIssue(
diff --git a/tests/integration/callbackhook/Parser.php
b/tests/integration/callbackhook/Parser.php
new file mode 100644
index 0000000..9def58e
--- /dev/null
+++ b/tests/integration/callbackhook/Parser.php
@@ -0,0 +1,64 @@
+<?php
+
+class Parser {
+
+ const MARKER_SUFFIX = "-QINU`\"'\x7f";
+ const MARKER_PREFIX = "\x7f'\"`UNIQ-";
+ /** @var int */
+ private $mMarkerIndex = 0;
+ /** @var StripState */
+ public $mStripState;
+ public function setFunctionHook( $id, callable $cb, $opts = 0 ) {
+ }
+
+ public function __construct() {
+ $this->mStripState = new StripState;
+ }
+ public function bar( Parser $parser, $arg1, $arg2 ) {
+ return $arg1;
+ }
+
+ public function insertStripItem( $text ) {
+ $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-"
. self::MARKER_SUFFIX;
+ $this->mMarkerIndex++;
+ $this->mStripState->addGeneral( $marker, $text );
+ return $marker;
+ }
+
+}
+
+class StripState {
+ protected $prefix;
+ protected $data;
+ protected $regex;
+
+ protected $tempType, $tempMergePrefix;
+ protected $circularRefGuard;
+ protected $recursionLevel = 0;
+
+ const UNSTRIP_RECURSION_LIMIT = 20;
+ protected function addItem( $type, $marker, $value ) {
+ if ( !preg_match( $this->regex, $marker, $m ) ) {
+ throw new Exception( "Invalid marker: $marker" );
+ }
+
+ $this->data[$type][$m[1]] = $value;
+ }
+
+ /**
+ * Add a nowiki strip item
+ * @param string $marker
+ * @param string $value
+ */
+ public function addNoWiki( $marker, $value ) {
+ $this->addItem( 'nowiki', $marker, $value );
+ }
+
+ /**
+ * @param string $marker
+ * @param string $value
+ */
+ public function addGeneral( $marker, $value ) {
+ $this->addItem( 'general', $marker, $value );
+ }
+}
diff --git a/tests/integration/callbackhook/db.php
b/tests/integration/callbackhook/db.php
new file mode 100644
index 0000000..93be506
--- /dev/null
+++ b/tests/integration/callbackhook/db.php
@@ -0,0 +1,20 @@
+<?php
+namespace Wikimedia\Rdbms;
+
+interface IDatabase {
+ public function query( $sql, $method );
+}
+
+class Database implements IDatabase {
+ public function query( $sql, $method = 'query' ) {
+ // do some stuff
+ return [ (object)[ 'some_field' => 'some value' ] ];
+ }
+
+}
+
+class MysqlDatabase extends Database {
+ public function getType() {
+ return 'mysql';
+ }
+}
diff --git a/tests/integration/callbackhook/expectedResults.txt
b/tests/integration/callbackhook/expectedResults.txt
new file mode 100644
index 0000000..550ef70
--- /dev/null
+++ b/tests/integration/callbackhook/expectedResults.txt
@@ -0,0 +1,7 @@
+integration/callbackhook/func.php:5 SecurityCheckTaintedOutput Outputting evil
HTML from Parser function hook \wfSomeFunc (Caused by:
integration/callbackhook/func.php +3; integration/callbackhook/func.php +4)
+integration/callbackhook/func.php:5 SecurityCheckTaintedOutput Outputting evil
HTML from Parser function hook \wfSomeFunc (Caused by:
integration/callbackhook/func.php +3; integration/callbackhook/func.php +4;
integration/callbackhook/func.php +3; integration/callbackhook/func.php +4)
+integration/callbackhook/test.php:12 SecurityCheckTaintedOutput Outputting
evil HTML from Parser function hook \closure_eabad951a64c
+integration/callbackhook/test.php:20 SecurityCheckTaintedOutput Outputting
evil HTML from Parser function hook \closure_fbf3d4f34852
+integration/callbackhook/test.php:30 SecurityCheckTaintedOutput Calling Method
\Wikimedia\Rdbms\MysqlDatabase::query in \MyNS\SomeClass::bar that outputs
using tainted (2040; 64) argument $[arg #1]. (Caused by:
integration/callbackhook/test.php +27; integration/callbackhook/test.php +30;
integration/callbackhook/test.php +30)
+integration/callbackhook/test.php:30 SecurityCheckTaintedOutput Calling Method
\Wikimedia\Rdbms\MysqlDatabase::query in \MyNS\SomeClass::bar that outputs
using tainted (680; 64) argument $[arg #1]. (Caused by:
integration/callbackhook/test.php +27; integration/callbackhook/test.php +30)
+integration/callbackhook/test.php:40 SecurityCheckTaintedOutput Outputting
evil HTML from Parser function hook \MyNS\SomeClass::baz (Caused by:
integration/callbackhook/test.php +39)
diff --git a/tests/integration/callbackhook/func.php
b/tests/integration/callbackhook/func.php
new file mode 100644
index 0000000..eef9f60
--- /dev/null
+++ b/tests/integration/callbackhook/func.php
@@ -0,0 +1,6 @@
+<?php
+function wfSomeFunc() {
+ $someEvil = $_GET['d'];
+ $someEvil .= "More text";
+ return [ $someEvil, 'isHTML' => true ];
+}
diff --git a/tests/integration/callbackhook/test.php
b/tests/integration/callbackhook/test.php
new file mode 100644
index 0000000..148501f
--- /dev/null
+++ b/tests/integration/callbackhook/test.php
@@ -0,0 +1,47 @@
+<?php
+namespace MyNS;
+
+use \Parser;
+use Wikimedia\Rdbms\MysqlDatabase;
+
+class SomeClass {
+ public function register() {
+ $parser = new Parser;
+
+ $indirectClosure = function ( Parser $parser, $arg ) {
+ return [ $arg, 'isHTML' => true ];
+ };
+
+ $parser->setFunctionHook( 'something', [ __CLASS__, 'bar' ] );
+ $parser->setFunctionHook( 'something', [ __CLASS__, 'baz' ] );
+
+ $parser->setFunctionHook( 'afunc', 'wfSomeFunc' );
+ $parser->setFunctionHook( 'two', function ( Parser $parser,
$arg1 ) {
+ return [ $_GET['d'], 'isHTML' => true ];
+ }
+ );
+
+ $parser->setFunctionHook( 'three', $indirectClosure );
+ }
+
+ public function bar( Parser $parser, $arg1, $arg2 ) {
+ $out = '';
+ $db = new MysqlDatabase;
+ $res = $db->query( "Select foo from someTable WHERE field =
'$arg2' LIMIT 1" );
+ foreach ( $res as $row ) {
+ $out .= $row->foo;
+ }
+ $out .= $arg1;
+
+ return $out;
+ }
+
+ public function baz( Parser $parser, $arg1 ) {
+ return [ $arg1, 'isHTML' => true ];
+ }
+}
+
+function wfSomeFunc() {
+ // This should not trigger because its in wrong NS
+ return [ $_GET['d'], 'isHTML' => true ];
+}
diff --git a/tests/integration/stripitem/Parser.php
b/tests/integration/stripitem/Parser.php
new file mode 100644
index 0000000..ac3ebc1
--- /dev/null
+++ b/tests/integration/stripitem/Parser.php
@@ -0,0 +1,61 @@
+<?php
+
+class Parser {
+
+ const MARKER_SUFFIX = "-QINU`\"'\x7f";
+ const MARKER_PREFIX = "\x7f'\"`UNIQ-";
+ /** @var int */
+ private $mMarkerIndex = 0;
+ /** @var StripState */
+ public $mStripState;
+ public function setFunctionHook( $id, callable $cb, $opts = 0 ) {
+ }
+
+ public function __construct() {
+ $this->mStripState = new StripState;
+ }
+
+ public function insertStripItem( $text ) {
+ $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-"
. self::MARKER_SUFFIX;
+ $this->mMarkerIndex++;
+ $this->mStripState->addGeneral( $marker, $text );
+ return $marker;
+ }
+
+}
+
+class StripState {
+ protected $prefix;
+ protected $data;
+ protected $regex;
+
+ protected $tempType, $tempMergePrefix;
+ protected $circularRefGuard;
+ protected $recursionLevel = 0;
+
+ const UNSTRIP_RECURSION_LIMIT = 20;
+ protected function addItem( $type, $marker, $value ) {
+ if ( !preg_match( $this->regex, $marker, $m ) ) {
+ throw new Exception( "Invalid marker: $marker" );
+ }
+
+ $this->data[$type][$m[1]] = $value;
+ }
+
+ /**
+ * Add a nowiki strip item
+ * @param string $marker
+ * @param string $value
+ */
+ public function addNoWiki( $marker, $value ) {
+ $this->addItem( 'nowiki', $marker, $value );
+ }
+
+ /**
+ * @param string $marker
+ * @param string $value
+ */
+ public function addGeneral( $marker, $value ) {
+ $this->addItem( 'general', $marker, $value );
+ }
+}
diff --git a/tests/integration/stripitem/expectedResults.txt
b/tests/integration/stripitem/expectedResults.txt
new file mode 100644
index 0000000..e5e03e8
--- /dev/null
+++ b/tests/integration/stripitem/expectedResults.txt
@@ -0,0 +1,4 @@
+integration/stripitem/test.php:11 SecurityCheckTaintedOutput Calling Method
\Parser::insertStripItem in \SomeClass::xyzzy that outputs using tainted (2040;
1360) argument $arg1. (Caused by: integration/stripitem/Parser.php +22;
integration/stripitem/Parser.php +22) (Caused by:
integration/stripitem/test.php +10; integration/stripitem/test.php +11;
integration/stripitem/test.php +11)
+integration/stripitem/test.php:11 SecurityCheckTaintedOutput Calling Method
\Parser::insertStripItem in \SomeClass::xyzzy that outputs using tainted (2040;
1360) argument $arg1. (Caused by: integration/stripitem/Parser.php +22;
integration/stripitem/Parser.php +22; integration/stripitem/Parser.php +22)
(Caused by: integration/stripitem/test.php +10; integration/stripitem/test.php
+11; integration/stripitem/test.php +11)
+integration/stripitem/test.php:11 SecurityCheckTaintedOutput Calling Method
\Parser::insertStripItem in \SomeClass::xyzzy that outputs using tainted (680;
1360) argument $arg1. (Caused by: integration/stripitem/Parser.php +22;
integration/stripitem/Parser.php +22) (Caused by:
integration/stripitem/test.php +10; integration/stripitem/test.php +11)
+integration/stripitem/test.php:11 SecurityCheckTaintedOutput Calling Method
\Parser::insertStripItem in \SomeClass::xyzzy that outputs using tainted (680;
1360) argument $arg1. (Caused by: integration/stripitem/Parser.php +22;
integration/stripitem/Parser.php +22; integration/stripitem/Parser.php +22)
(Caused by: integration/stripitem/test.php +10; integration/stripitem/test.php
+11)
diff --git a/tests/integration/stripitem/test.php
b/tests/integration/stripitem/test.php
new file mode 100644
index 0000000..ba8aaf5
--- /dev/null
+++ b/tests/integration/stripitem/test.php
@@ -0,0 +1,14 @@
+<?php
+
+class SomeClass {
+ public function register() {
+ $parser = new Parser;
+
+ $parser->setFunctionHook( 'something', [ __CLASS__, 'xyzzy' ] );
+ }
+
+ public function xyzzy( Parser $parser, $arg1 ) {
+ $a = $parser->insertStripItem( $arg1 );
+ return $a;
+ }
+}
diff --git a/tests/runtests.sh b/tests/runtests.sh
index d5bcd5c..a3de9af 100755
--- a/tests/runtests.sh
+++ b/tests/runtests.sh
@@ -1,18 +1,25 @@
#!/bin/bash
-# If you want to see debug output
-# call with SECCHECK_DEBUG=/dev/stderr
-# environment variable.
#
-# If you want to run a specific test
-# then give it as first arg.
+# Run the tests in integration.
+# Usage: ./runtests.sh [-v] [testname]
+#
+# -v will add debug output. testname will run only that test
+#
+
cd `dirname $0`/
-testList=${1:-`ls integration`}
tmpFile=`mktemp testtmp.XXXXXXXX`
+trap "rm $tmpFile" exit
totalTests=0
failedTests=0
php=`which php7.0`
php=${php:-`which php`}
-SECCHECK_DEBUG=${SECCHECK_DEBUG:-/dev/null}
+if [ "$1" = '-v' ]
+ then SECCHECK_DEBUG=/dev/stderr
+ shift
+ export SECCHECK_DEBUG
+ else SECCHECK_DEBUG=${SECCHECK_DEBUG:-/dev/null}
+fi
+testList=${1:-`ls integration`}
for i in $testList
do
@@ -28,7 +35,6 @@
then failedTests=$((failedTests+1))
fi
done
-rm $tmpFile
if [ $failedTests -gt 0 ]
then echo $failedTests out of $totalTests failed.
exit 1
--
To view, visit https://gerrit.wikimedia.org/r/393540
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Idcd74eee6ad5fb38d7c8b81f4cf1058dc26f080b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/tools/phan/SecurityCheckPlugin
Gerrit-Branch: master
Gerrit-Owner: Brian Wolff <[email protected]>
Gerrit-Reviewer: Brian Wolff <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits