Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 147 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
| TaintednessAssignVisitor | |
0.00% |
0 / 147 |
|
0.00% |
0 / 9 |
2256 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| isRHSArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| visitArray | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
| visitVar | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| visitProp | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| visitStaticProp | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| maybeAddNumkeyOnAssignmentLHS | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
| visitDim | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
| doAssignmentSingleElement | |
0.00% |
0 / 72 |
|
0.00% |
0 / 1 |
462 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace SecurityCheckPlugin; |
| 4 | |
| 5 | use ast\Node; |
| 6 | use Closure; |
| 7 | use Phan\CodeBase; |
| 8 | use Phan\Exception\IssueException; |
| 9 | use Phan\Exception\NodeException; |
| 10 | use Phan\Exception\UnanalyzableException; |
| 11 | use Phan\Language\Context; |
| 12 | use Phan\Language\Element\GlobalVariable; |
| 13 | use Phan\Language\Element\Property; |
| 14 | use Phan\Language\Element\TypedElementInterface; |
| 15 | use Phan\PluginV3\PluginAwareBaseAnalysisVisitor; |
| 16 | |
| 17 | /** |
| 18 | * @see \Phan\Analysis\AssignmentVisitor |
| 19 | */ |
| 20 | class TaintednessAssignVisitor extends PluginAwareBaseAnalysisVisitor { |
| 21 | use TaintednessBaseVisitor; |
| 22 | |
| 23 | /** @var Taintedness */ |
| 24 | private $rightTaint; |
| 25 | |
| 26 | /** @var Taintedness */ |
| 27 | private $errorTaint; |
| 28 | |
| 29 | /** @var MethodLinks */ |
| 30 | private $errorLinks; |
| 31 | |
| 32 | /** @var CausedByLines */ |
| 33 | private $rightError; |
| 34 | |
| 35 | /** @var MethodLinks */ |
| 36 | private $rightLinks; |
| 37 | |
| 38 | /** @var bool|null */ |
| 39 | private $rhsIsArray; |
| 40 | |
| 41 | /** @var Closure|null */ |
| 42 | private $rhsIsArrayGetter; |
| 43 | |
| 44 | /** @var int */ |
| 45 | private $dimDepth; |
| 46 | |
| 47 | /** |
| 48 | * @inheritDoc |
| 49 | * @param Taintedness $rightTaint |
| 50 | * @param CausedByLines $rightLines |
| 51 | * @param MethodLinks $rightLinks |
| 52 | * @param Taintedness $errorTaint |
| 53 | * @param MethodLinks $errorLinks |
| 54 | * @param Closure|bool $rhsIsArrayOrGetter |
| 55 | * @phan-param Closure():bool|bool $rhsIsArrayOrGetter |
| 56 | * @param int $depth |
| 57 | */ |
| 58 | public function __construct( |
| 59 | CodeBase $code_base, |
| 60 | Context $context, |
| 61 | Taintedness $rightTaint, |
| 62 | CausedByLines $rightLines, |
| 63 | MethodLinks $rightLinks, |
| 64 | Taintedness $errorTaint, |
| 65 | MethodLinks $errorLinks, |
| 66 | $rhsIsArrayOrGetter, |
| 67 | int $depth = 0 |
| 68 | ) { |
| 69 | parent::__construct( $code_base, $context ); |
| 70 | $this->rightTaint = $rightTaint; |
| 71 | $this->rightError = $rightLines; |
| 72 | $this->rightLinks = $rightLinks; |
| 73 | $this->errorTaint = $errorTaint; |
| 74 | $this->errorLinks = $errorLinks; |
| 75 | if ( is_callable( $rhsIsArrayOrGetter ) ) { |
| 76 | $this->rhsIsArrayGetter = $rhsIsArrayOrGetter; |
| 77 | } else { |
| 78 | $this->rhsIsArray = $rhsIsArrayOrGetter; |
| 79 | } |
| 80 | $this->dimDepth = $depth; |
| 81 | } |
| 82 | |
| 83 | private function isRHSArray(): bool { |
| 84 | if ( $this->rhsIsArray !== null ) { |
| 85 | return $this->rhsIsArray; |
| 86 | } |
| 87 | $this->rhsIsArray = ( $this->rhsIsArrayGetter )(); |
| 88 | return $this->rhsIsArray; |
| 89 | } |
| 90 | |
| 91 | public function visitArray( Node $node ): void { |
| 92 | $numKey = 0; |
| 93 | foreach ( $node->children as $child ) { |
| 94 | if ( $child === null ) { |
| 95 | $numKey++; |
| 96 | continue; |
| 97 | } |
| 98 | if ( !$child instanceof Node || $child->kind !== \ast\AST_ARRAY_ELEM ) { |
| 99 | // Syntax error. |
| 100 | return; |
| 101 | } |
| 102 | $key = $child->children['key'] !== null ? $this->resolveOffset( $child->children['key'] ) : $numKey++; |
| 103 | $value = $child->children['value']; |
| 104 | if ( !$value instanceof Node ) { |
| 105 | // Syntax error, don't crash, and bail out immediately. |
| 106 | return; |
| 107 | } |
| 108 | $childVisitor = new self( |
| 109 | $this->code_base, |
| 110 | $this->context, |
| 111 | $this->rightTaint->getTaintednessForOffsetOrWhole( $key ), |
| 112 | $this->rightError->getForDim( $key ), |
| 113 | $this->rightLinks, |
| 114 | $this->errorTaint->getTaintednessForOffsetOrWhole( $key ), |
| 115 | $this->errorLinks, |
| 116 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
| 117 | $this->rhsIsArray ?? $this->rhsIsArrayGetter, |
| 118 | $this->dimDepth |
| 119 | ); |
| 120 | $childVisitor( $value ); |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | public function visitVar( Node $node ): void { |
| 125 | try { |
| 126 | $var = $this->getCtxN( $node )->getVariable(); |
| 127 | } catch ( NodeException | IssueException ) { |
| 128 | return; |
| 129 | } |
| 130 | $this->doAssignmentSingleElement( $var ); |
| 131 | } |
| 132 | |
| 133 | public function visitProp( Node $node ): void { |
| 134 | try { |
| 135 | $prop = $this->getCtxN( $node )->getProperty( false ); |
| 136 | } catch ( NodeException | IssueException | UnanalyzableException ) { |
| 137 | return; |
| 138 | } |
| 139 | $this->doAssignmentSingleElement( $prop ); |
| 140 | } |
| 141 | |
| 142 | public function visitStaticProp( Node $node ): void { |
| 143 | try { |
| 144 | $prop = $this->getCtxN( $node )->getProperty( true ); |
| 145 | } catch ( NodeException | IssueException | UnanalyzableException ) { |
| 146 | return; |
| 147 | } |
| 148 | $this->doAssignmentSingleElement( $prop ); |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * If we're assigning an SQL tainted value as an array key |
| 153 | * or as the value of a numeric key, then set NUMKEY taint. |
| 154 | */ |
| 155 | private function maybeAddNumkeyOnAssignmentLHS( Node $dimLHS ): void { |
| 156 | if ( $this->rightTaint->has( SecurityCheckPlugin::SQL_NUMKEY_TAINT ) ) { |
| 157 | // Already there, no need to add it again. |
| 158 | return; |
| 159 | } |
| 160 | |
| 161 | $dim = $dimLHS->children['dim']; |
| 162 | if ( |
| 163 | $this->rightTaint->has( SecurityCheckPlugin::SQL_TAINT ) |
| 164 | && ( $dim === null || $this->nodeCanBeIntKey( $dim ) ) |
| 165 | && !$this->isRHSArray() |
| 166 | ) { |
| 167 | $this->rightTaint = $this->rightTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
| 168 | $this->errorTaint = $this->errorTaint->with( SecurityCheckPlugin::SQL_NUMKEY_TAINT ); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | public function visitDim( Node $node ): void { |
| 173 | if ( !$node->children['expr'] instanceof Node ) { |
| 174 | // Invalid syntax. |
| 175 | return; |
| 176 | } |
| 177 | $dimNode = $node->children['dim']; |
| 178 | if ( $dimNode === null ) { |
| 179 | $curOff = null; |
| 180 | } else { |
| 181 | $curOff = $this->resolveOffset( $dimNode ); |
| 182 | } |
| 183 | $this->dimDepth++; |
| 184 | $dimTaintWithErr = $this->getTaintedness( $dimNode ); |
| 185 | $dimTaintInt = $dimTaintWithErr->getTaintedness()->get(); |
| 186 | $this->rightTaint = $this->rightTaint->asMaybeMovedAtOffset( $curOff, $dimTaintInt ); |
| 187 | $dimLinks = $dimTaintWithErr->getMethodLinks()->getLinksCollapsing(); |
| 188 | $this->rightLinks = $this->rightLinks->asMaybeMovedAtOffset( $curOff, $dimLinks ); |
| 189 | $dimError = $dimTaintWithErr->getError()->asAllMovedToKeys(); |
| 190 | $this->rightError = $this->rightError->asAllMaybeMovedAtOffset( $curOff )->asMergedWith( $dimError ); |
| 191 | $this->errorTaint = $this->errorTaint->asMaybeMovedAtOffset( $curOff, $dimTaintInt ); |
| 192 | $this->errorLinks = $this->errorLinks->asMaybeMovedAtOffset( $curOff, $dimLinks ); |
| 193 | $this->maybeAddNumkeyOnAssignmentLHS( $node ); |
| 194 | $this( $node->children['expr'] ); |
| 195 | } |
| 196 | |
| 197 | private function doAssignmentSingleElement( |
| 198 | TypedElementInterface $variableObj |
| 199 | ): void { |
| 200 | $globalVarObj = $variableObj instanceof GlobalVariable ? $variableObj->getElement() : null; |
| 201 | |
| 202 | // Make sure assigning to $this->bar doesn't kill the whole prop taint. |
| 203 | // Note: If there is a local variable that is a reference to another non-local variable, this will not |
| 204 | // affect the non-local one (Pass by reference arguments are handled separately and work as expected). |
| 205 | // TODO Should we also check for normal Variables in the global scope? See test setafterexec |
| 206 | $override = !( $variableObj instanceof Property ) && !$globalVarObj; |
| 207 | |
| 208 | $overrideTaint = $override; |
| 209 | if ( $this->dimDepth > 0 ) { |
| 210 | $curTaint = self::getTaintednessRaw( $variableObj ); |
| 211 | if ( $curTaint ) { |
| 212 | $newTaint = $override |
| 213 | ? $curTaint->asMergedForAssignment( $this->rightTaint, $this->dimDepth ) |
| 214 | : $curTaint->asMergedWith( $this->rightTaint ); |
| 215 | } else { |
| 216 | $newTaint = $this->rightTaint; |
| 217 | } |
| 218 | $overrideTaint = true; |
| 219 | } else { |
| 220 | $newTaint = $this->rightTaint; |
| 221 | } |
| 222 | $this->setTaintedness( $variableObj, $newTaint, $overrideTaint ); |
| 223 | |
| 224 | if ( $globalVarObj ) { |
| 225 | // Merge the taint on the "true" global object, too |
| 226 | if ( $this->dimDepth > 0 ) { |
| 227 | $curGlobalTaint = self::getTaintednessRaw( $globalVarObj ); |
| 228 | if ( $curGlobalTaint ) { |
| 229 | $newGlobalTaint = $curGlobalTaint->asMergedWith( $this->rightTaint ); |
| 230 | } else { |
| 231 | $newGlobalTaint = $this->rightTaint; |
| 232 | } |
| 233 | $overrideGlobalTaint = true; |
| 234 | } else { |
| 235 | $newGlobalTaint = $this->rightTaint; |
| 236 | $overrideGlobalTaint = false; |
| 237 | } |
| 238 | $this->setTaintedness( $globalVarObj, $newGlobalTaint, $overrideGlobalTaint ); |
| 239 | } |
| 240 | |
| 241 | if ( $this->dimDepth > 0 ) { |
| 242 | $curLinks = self::getMethodLinks( $variableObj ) ?? MethodLinks::emptySingleton(); |
| 243 | $newLinks = $override |
| 244 | ? $curLinks->asMergedForAssignment( $this->rightLinks, $this->dimDepth ) |
| 245 | : $curLinks->asMergedWith( $this->rightLinks ); |
| 246 | $overrideLinks = true; |
| 247 | } else { |
| 248 | $newLinks = $this->rightLinks; |
| 249 | $overrideLinks = $override; |
| 250 | } |
| 251 | $this->mergeTaintDependencies( $variableObj, $newLinks, $overrideLinks ); |
| 252 | if ( $globalVarObj ) { |
| 253 | // Merge dependencies on the global copy as well |
| 254 | if ( $this->dimDepth > 0 ) { |
| 255 | $curGlobalLinks = self::getMethodLinks( $globalVarObj ); |
| 256 | $newGlobalLinks = $curGlobalLinks |
| 257 | ? $curGlobalLinks->asMergedWith( $this->rightLinks ) |
| 258 | : $this->rightLinks; |
| 259 | $overrideGlobalLinks = true; |
| 260 | } else { |
| 261 | $newGlobalLinks = $this->rightLinks; |
| 262 | $overrideGlobalLinks = false; |
| 263 | } |
| 264 | $this->mergeTaintDependencies( $globalVarObj, $newGlobalLinks, $overrideGlobalLinks ); |
| 265 | } |
| 266 | |
| 267 | $curLineCausedBy = $this->getCausedByLinesToAdd( $this->errorTaint, $this->errorLinks ); |
| 268 | if ( $this->dimDepth > 0 ) { |
| 269 | $curError = self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton(); |
| 270 | $newError = $override |
| 271 | ? $curError->asMergedWith( $this->rightError, $this->dimDepth ) |
| 272 | : $curError->asMergedWith( $this->rightError ); |
| 273 | $overrideError = true; |
| 274 | } else { |
| 275 | $newError = $this->rightError; |
| 276 | $overrideError = $override; |
| 277 | } |
| 278 | $curError = $overrideError |
| 279 | ? CausedByLines::emptySingleton() |
| 280 | : self::getCausedByRaw( $variableObj ) ?? CausedByLines::emptySingleton(); |
| 281 | $newOverallError = $curError |
| 282 | ->asMergedForAssignment( $newError, $curLineCausedBy, $this->errorTaint, $this->errorLinks ); |
| 283 | self::setCausedByRaw( $variableObj, $newOverallError ); |
| 284 | |
| 285 | if ( $globalVarObj ) { |
| 286 | if ( $this->dimDepth > 0 ) { |
| 287 | $curGlobalError = self::getCausedByRaw( $globalVarObj ); |
| 288 | $newGlobalError = $curGlobalError |
| 289 | ? $curGlobalError->asMergedWith( $this->rightError ) |
| 290 | : $this->rightError; |
| 291 | $overrideGlobalError = true; |
| 292 | } else { |
| 293 | $newGlobalError = $this->rightError; |
| 294 | $overrideGlobalError = false; |
| 295 | } |
| 296 | $curGlobalError = $overrideGlobalError |
| 297 | ? CausedByLines::emptySingleton() |
| 298 | : self::getCausedByRaw( $globalVarObj ) ?? CausedByLines::emptySingleton(); |
| 299 | $newOverallGlobalError = $curGlobalError |
| 300 | ->asMergedForAssignment( $newGlobalError, $curLineCausedBy, $this->errorTaint, $this->errorLinks ); |
| 301 | self::setCausedByRaw( $globalVarObj, $newOverallGlobalError ); |
| 302 | } |
| 303 | } |
| 304 | } |