Brian Wolff has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/395757 )
Change subject: Ensure that errors related to function are per param ...................................................................... Ensure that errors related to function are per param Previously there was some misleading issues issued due to all the arguments to a function being conflated. See the json integration test for an example. Change-Id: Ib184935ba76d128f8165dd467dc12bbc86acf955 --- M src/TaintednessBaseVisitor.php M tests/integration/json/expectedResults.txt M tests/integration/registerhook/expectedResults.txt 3 files changed, 109 insertions(+), 62 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/tools/phan/SecurityCheckPlugin refs/changes/57/395757/1 diff --git a/src/TaintednessBaseVisitor.php b/src/TaintednessBaseVisitor.php index c87b47e..a1957d2 100644 --- a/src/TaintednessBaseVisitor.php +++ b/src/TaintednessBaseVisitor.php @@ -59,10 +59,6 @@ /** * Change taintedness of a function/method * - * @todo FIXME, the taintedOriginalError property should be on - * a per parameter granuality, instead of per function. As it - * stands this means very misleading error messages can be - * output. See the json integration test for an example. * @param FunctionInterface $func * @param int[] $taint Numeric keys for each arg and an 'overall' key. * @param bool $override Whether to merge taint or override @@ -99,6 +95,11 @@ $newTaint[$index] = ( $curTaint[$index] ?? 0 ) | $t; } $mergedTaint |= $t; + if ( is_int( $index ) ) { + $this->addTaintError( $t, $func, $index ); + } else { + $this->addTaintError( $t, $func ); + } } if ( !isset( $newTaint['overall'] ) ) { // FIXME, what's the right default?? @@ -107,22 +108,6 @@ } $this->checkFuncTaint( $newTaint ); $func->funcTaint = $newTaint; - - if ( $mergedTaint & SecurityCheckPlugin::YES_EXEC_TAINT ) { - if ( !property_exists( $func, 'taintedOriginalError' ) ) { - $func->taintedOriginalError = ''; - } - $newError = $this->dbgInfo() . ';'; - if ( strpos( $func->taintedOriginalError, $newError ) === false ) { - // Only add to error if we haven't added this line before - $func->taintedOriginalError .= $newError; - } - - if ( strlen( $func->taintedOriginalError ) > 254 ) { - $this->debug( __METHOD__, "Too long original error! for $func" ); - $func->taintedOriginalError = substr( $func->taintedOriginalError, 0, 250 ) . '...'; - } - } } /** @@ -148,7 +133,69 @@ if ( strlen( $left->taintedOriginalError ) > 254 ) { $this->debug( __METHOD__, "Too long original error! for $left" ); - $left->taintedOriginalError = substr( $left->taintedOriginalError, 0, 250 ) . '...'; + $left->taintedOriginalError = substr( $left->taintedOriginalError, 0, 250 ) . '... '; + } + } + + /** + * Add the current context to taintedOriginalError book-keeping + * + * This allows us to show users what line caused an issue. + * + * @param int $taintedness The taintedness in question + * @param TypedElementInterface $elem Where to put it + * @param int $arg [Optional] For functions, which argument + */ + protected function addTaintError( int $taintedness, TypedElementInterface $elem, int $arg = -1 ) { + if ( !$this->isExecTaint( $taintedness ) && !$this->isYesTaint( $taintedness ) ) { + // Don't add book-keeping if no actual taint was added. + return; + } + + assert( $arg === -1 || $elem instanceof FunctionInterface ); + + if ( $arg === -1 ) { + if ( !property_exists( $elem, 'taintedOriginalError' ) ) { + $elem->taintedOriginalError = ''; + } + } else { + if ( !property_exists( $elem, 'taintedOriginalErrorByArg' ) ) { + $elem->taintedOriginalErrorByArg = []; + } + if ( !isset( $elem->taintedOriginalErrorByArg[$arg] ) ) { + $elem->taintedOriginalErrorByArg[$arg] = ''; + } + } + $newErrors = [ $this->dbgInfo() . ';' ]; + if ( $this->overrideContext ) { + $newErrors[] = $this->dbgInfo( $this->overrideContext ) . ';'; + } + foreach ( $newErrors as $newError ) { + if ( $arg === -1 ) { + if ( strpos( $elem->taintedOriginalError, $newError ) === false ) { + $elem->taintedOriginalError .= $newError; + } + } else { + if ( strpos( $elem->taintedOriginalErrorByArg[$arg], $newError ) === false ) { + $elem->taintedOriginalErrorByArg[$arg] .= $newError; + } + } + } + + if ( $arg === -1 ) { + if ( strlen( $elem->taintedOriginalError ) > 254 ) { + $this->debug( __METHOD__, "Too long original error! $elem" ); + $elem->taintedOriginalError = substr( + $elem->taintedOriginalError, 0, 250 + ) . '... '; + } + } else { + if ( strlen( $elem->taintedOriginalErrorByArg[$arg] ) > 254 ) { + $this->debug( __METHOD__, "Too long original error! $elem" ); + $elem->taintedOriginalErrorByArg[$arg] = substr( + $elem->taintedOriginalError[$arg], 0, 250 + ) . '... '; + } } } @@ -216,7 +263,7 @@ if ( strlen( $combinedOrig ) > 254 ) { $this->debug( __METHOD__, "Too long original error! $variableObj" ); - $combinedOrig = substr( $combinedOrig, 0, 250 ) . '...'; + $combinedOrig = substr( $combinedOrig, 0, 250 ) . '... '; } $variableObj->taintedOriginalError = $combinedOrig; $parentVarObj->taintedOriginalError =& $variableObj->taintedOriginalError; @@ -244,27 +291,7 @@ ); } - if ( $this->isExecTaint( $taintedness ) || $this->isYesTaint( $taintedness ) ) { - if ( !property_exists( $variableObj, 'taintedOriginalError' ) ) { - $variableObj->taintedOriginalError = ''; - } - $newErrors = [ $this->dbgInfo() . ';' ]; - if ( $this->overrideContext ) { - $newErrors[] = $this->dbgInfo( $this->overrideContext ) . ';'; - } - foreach ( $newErrors as $newError ) { - if ( strpos( $variableObj->taintedOriginalError, $newError ) === false ) { - $variableObj->taintedOriginalError .= $newError; - } - } - - if ( strlen( $variableObj->taintedOriginalError ) > 254 ) { - $this->debug( __METHOD__, "Too long original error! $variableObj" ); - $variableObj->taintedOriginalError = substr( - $variableObj->taintedOriginalError, 0, 250 - ) . '...'; - } - } + $this->addTaintError( $taintedness, $variableObj ); } /** @@ -998,22 +1025,49 @@ * Get the line number of the original cause of taint. * * @param TypedElementInterface|Node $element + * @param int $arg [optional] For functions what arg. -1 for overall. * @return string */ - protected function getOriginalTaintLine( $element ) { + protected function getOriginalTaintLine( $element, $arg = -1 ) { + $line = $this->getOriginalTaintLineRaw( $element, $arg ); + if ( $line ) { + $line = substr( $line, 0, strlen( $line ) - 1 ); + return " (Caused by:$line)"; + } else { + return ''; + } + } + + /** + * Get the line number of the original cause of taint without "Caused by" string. + * + * @param TypedElementInterface|Node $element + * @param int $arg [optional] For functions what arg. -1 for overall. + * @return string + */ + private function getOriginalTaintLineRaw( $element, $arg = - 1 ) { $line = ''; if ( !is_object( $element ) ) { return ''; } elseif ( $element instanceof TypedElementInterface ) { - if ( property_exists( $element, 'taintedOriginalError' ) ) { - $line = $element->taintedOriginalError; + if ( $arg === -1 ) { + if ( property_exists( $element, 'taintedOriginalError' ) ) { + $line = $element->taintedOriginalError; + } + foreach ( $element->taintedOriginalErrorByArg ?? [] as $arg ) { + // FIXME is this right? In the generic + // case should we include all arguments as + // well? + $line .= $arg; + } + } else { + assert( $element instanceof FunctionInterface ); + $line .= $element->taintedOriginalErrorByArg[$arg] ?? ''; } } elseif ( $element instanceof Node ) { $pobjs = $this->getPhanObjsForNode( $element ); foreach ( $pobjs as $elem ) { - if ( property_exists( $elem, 'taintedOriginalError' ) ) { - $line .= $elem->taintedOriginalError; - } + $line .= $this->getOriginalTaintLineRaw( $elem ); } if ( $line === '' ) { // try to dig deeper. @@ -1021,9 +1075,7 @@ // FIXME should we always do this? Is it too spammy. $pobjs = $this->getPhanObjsForNode( $element, true ); foreach ( $pobjs as $elem ) { - if ( property_exists( $elem, 'taintedOriginalError' ) ) { - $line .= $elem->taintedOriginalError; - } + $line .= $this->getOriginalTaintLineRaw( $elem ); } } } else { @@ -1033,12 +1085,7 @@ ); } assert( strlen( $line ) < 8096, " taint error too long $line" ); - if ( $line ) { - $line = substr( $line, 0, strlen( $line ) - 1 ); - return " (Caused by:$line)"; - } else { - return ''; - } + return $line; } /** @@ -1552,7 +1599,7 @@ $combinedOrig = ( $pobj->taintedOriginalError ?? '' ) . ( $methodVar->taintedOriginalError ?? '' ); if ( strlen( $combinedOrig ) > 255 ) { - $combinedOrig = substr( $combinedOrig, 0, 250 ) . '...'; + $combinedOrig = substr( $combinedOrig, 0, 250 ) . '... '; } $pobj->taintedOriginalError = $combinedOrig; $methodVar->taintedOriginalError =& $pobj->taintedOriginalError; @@ -1603,7 +1650,7 @@ $curArgTaintedness, "Calling method $funcName() in $containingMethod" . " that outputs using tainted argument \$$taintedArg." . - ( $func ? $this->getOriginalTaintLine( $func ) : '' ) . + ( $func ? $this->getOriginalTaintLine( $func, $i ) : '' ) . $this->getOriginalTaintLine( $argument ) ); diff --git a/tests/integration/json/expectedResults.txt b/tests/integration/json/expectedResults.txt index f48a8d4..af1a62b 100644 --- a/tests/integration/json/expectedResults.txt +++ b/tests/integration/json/expectedResults.txt @@ -1,2 +1,2 @@ -integration/json/test.php:9 SecurityCheck-XSS Calling method \wfRegister() in \doStuff that outputs using tainted argument $[arg #2]. (Caused by: integration/json/test.php +10; integration/json/test.php +11) (Originally at: integration/json/test.php:29) +integration/json/test.php:9 SecurityCheck-XSS Calling method \wfRegister() in \doStuff that outputs using tainted argument $[arg #2]. (Caused by: integration/json/test.php +11) (Originally at: integration/json/test.php:29) integration/json/test.php:32 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/json/test.php +23) diff --git a/tests/integration/registerhook/expectedResults.txt b/tests/integration/registerhook/expectedResults.txt index 4550a47..767d740 100644 --- a/tests/integration/registerhook/expectedResults.txt +++ b/tests/integration/registerhook/expectedResults.txt @@ -1,5 +1,5 @@ integration/registerhook/test.php:29 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/registerhook/test.php +40; integration/registerhook/test.php +28; integration/registerhook/test.php +46; integration/registerhook/test.php +54) -integration/registerhook/test.php:30 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/registerhook/test.php +28; integration/registerhook/test.php +42; integration/registerhook/test.php +46; integration/registerhook/test.php +32; integration/registerhook/test.php +50; integration/registerhook/test.php +54; integration/reg..) +integration/registerhook/test.php:30 SecurityCheck-XSS Echoing expression that was not html escaped (Caused by: integration/registerhook/test.php +28; integration/registerhook/test.php +42; integration/registerhook/test.php +46; integration/registerhook/test.php +32; integration/registerhook/test.php +50; integration/registerhook/test.php +54; integration/reg...) integration/registerhook/test.php:52 SecurityCheck-XSS Echoing expression that was not html escaped integration/registerhook/test.php:61 SecurityCheck-XSS Echoing expression that was not html escaped integration/registerhook/test.php:65 SecurityCheck-OTHER Argument to require, include or eval is user controlled -- To view, visit https://gerrit.wikimedia.org/r/395757 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ib184935ba76d128f8165dd467dc12bbc86acf955 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