Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.06% covered (success)
94.06%
95 / 101
86.96% covered (warning)
86.96%
20 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
FunctionTaintedness
94.06% covered (success)
94.06%
95 / 101
86.96% covered (warning)
86.96%
20 / 23
65.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 emptySingleton
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 withOverall
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getOverall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canOverrideOverall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withParamSinkTaint
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 withParamPreservedTaint
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 withVariadicParamSinkTaint
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 withVariadicParamPreservedTaint
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getParamSinkTaint
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getParamPreservedTaint
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 getParamFlags
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 canOverrideNonVariadicParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVariadicParamSinkTaint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVariadicParamPreservedTaint
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariadicParamIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canOverrideVariadicParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSinkParamKeysNoVariadic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPreserveParamKeysNoVariadic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasParamPreserve
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 asMergedWith
88.24% covered (warning)
88.24%
30 / 34
0.00% covered (danger)
0.00%
0 / 1
14.32
 withoutPreserved
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 asOnlyPreserved
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 toString
n/a
0 / 0
n/a
0 / 0
7
 flagsToString
n/a
0 / 0
n/a
0 / 0
4
 __toString
n/a
0 / 0
n/a
0 / 0
1
1<?php declare( strict_types=1 );
2
3namespace SecurityCheckPlugin;
4
5/**
6 * Value object used to store taintedness of functions.
7 * The $overall prop specifies what taint the function returns
8 *   irrespective of its arguments.
9 *
10 *   For 'overall': only TAINT flags, what taint the output has
11 *   For param keys: EXEC flags for what taints are unsafe here
12 *                     TAINT flags for what taint gets passed through func.
13 * As a special case, if the overall key has self::PRESERVE_TAINT
14 * then any unspecified keys behave like they are self::YES_TAINT
15 *
16 * If func has no info for a parameter, the UnionType will be used to determine its taintedness.
17 * The $overall taintedness must always be set.
18 */
19class FunctionTaintedness {
20    /** @var Taintedness Overall taintedness of the func */
21    private $overall;
22    /** @var Taintedness[] EXEC taintedness for each param */
23    private $paramSinkTaints = [];
24    /** @var PreservedTaintedness[] Preserved taintedness for each param */
25    private $paramPreserveTaints = [];
26    /** @var int|null Index of a variadic parameter, if any */
27    private $variadicParamIndex;
28    /** @var Taintedness|null EXEC taintedness for a variadic parameter, if any */
29    private $variadicParamSinkTaint;
30    /** @var PreservedTaintedness|null Preserved taintedness for a variadic parameter, if any */
31    private $variadicParamPreserveTaint;
32    /** @var int Special overall flags */
33    private $overallFlags = 0;
34    /** @var int[] Special flags for parameters */
35    private $paramFlags = [];
36    /** @var int */
37    private $variadicParamFlags = 0;
38
39    /**
40     * @param Taintedness $overall
41     */
42    public function __construct( Taintedness $overall, int $overallFlags = 0 ) {
43        $this->overall = $overall;
44        $this->overallFlags = $overallFlags;
45    }
46
47    public static function emptySingleton(): self {
48        static $singleton;
49        if ( !$singleton ) {
50            $singleton = new self( Taintedness::safeSingleton() );
51        }
52        return $singleton;
53    }
54
55    public function withOverall( Taintedness $val, int $flags = 0 ): self {
56        $ret = clone $this;
57        $ret->overall = $val;
58        $ret->overallFlags |= $flags;
59        return $ret;
60    }
61
62    /**
63     * Get the overall taint (NOT a clone)
64     *
65     * @return Taintedness
66     */
67    public function getOverall(): Taintedness {
68        return $this->overall;
69    }
70
71    /**
72     * @return bool
73     */
74    public function canOverrideOverall(): bool {
75        return ( $this->overallFlags & SecurityCheckPlugin::NO_OVERRIDE ) === 0;
76    }
77
78    /**
79     * Set the sink taint for a given param
80     *
81     * @param int $param
82     * @param Taintedness $taint
83     * @param int $flags
84     * @return self
85     */
86    public function withParamSinkTaint( int $param, Taintedness $taint, int $flags = 0 ): self {
87        $ret = clone $this;
88        assert( $param !== $ret->variadicParamIndex );
89        $ret->paramSinkTaints[$param] = $taint;
90        $ret->paramFlags[$param] = ( $ret->paramFlags[$param] ?? 0 ) | $flags;
91        return $ret;
92    }
93
94    /**
95     * Set the preserved taint for a given param
96     *
97     * @param int $param
98     * @param PreservedTaintedness $taint
99     * @param int $flags
100     * @return self
101     */
102    public function withParamPreservedTaint( int $param, PreservedTaintedness $taint, int $flags = 0 ): self {
103        $ret = clone $this;
104        assert( $param !== $ret->variadicParamIndex );
105        $ret->paramPreserveTaints[$param] = $taint;
106        $ret->paramFlags[$param] = ( $ret->paramFlags[$param] ?? 0 ) | $flags;
107        return $ret;
108    }
109
110    public function withVariadicParamSinkTaint( int $index, Taintedness $taint, int $flags = 0 ): self {
111        $ret = clone $this;
112        assert( !isset( $ret->paramPreserveTaints[$index] ) && !isset( $ret->paramSinkTaints[$index] ) );
113        $ret->variadicParamIndex = $index;
114        $ret->variadicParamSinkTaint = $taint;
115        $ret->variadicParamFlags |= $flags;
116        return $ret;
117    }
118
119    public function withVariadicParamPreservedTaint( int $index, PreservedTaintedness $taint, int $flags = 0 ): self {
120        $ret = clone $this;
121        assert( !isset( $ret->paramPreserveTaints[$index] ) && !isset( $ret->paramSinkTaints[$index] ) );
122        $ret->variadicParamIndex = $index;
123        $ret->variadicParamPreserveTaint = $taint;
124        $ret->variadicParamFlags |= $flags;
125        return $ret;
126    }
127
128    /**
129     * Get the sink taintedness of the given param (NOT a clone), and NO_TAINT if not set.
130     *
131     * @param int $param
132     * @return Taintedness
133     */
134    public function getParamSinkTaint( int $param ): Taintedness {
135        if ( isset( $this->paramSinkTaints[$param] ) ) {
136            return $this->paramSinkTaints[$param];
137        }
138        if (
139            $this->variadicParamIndex !== null && $param >= $this->variadicParamIndex &&
140            $this->variadicParamSinkTaint
141        ) {
142            return $this->variadicParamSinkTaint;
143        }
144        return Taintedness::safeSingleton();
145    }
146
147    /**
148     * Get the preserved taintedness of the given param (NOT a clone), and NO_TAINT if not set.
149     *
150     * @param int $param
151     * @return PreservedTaintedness
152     */
153    public function getParamPreservedTaint( int $param ): PreservedTaintedness {
154        if ( isset( $this->paramPreserveTaints[$param] ) ) {
155            return $this->paramPreserveTaints[$param];
156        }
157        if (
158            $this->variadicParamIndex !== null && $param >= $this->variadicParamIndex &&
159            $this->variadicParamPreserveTaint
160        ) {
161            return $this->variadicParamPreserveTaint;
162        }
163        return PreservedTaintedness::emptySingleton();
164    }
165
166    /**
167     * @param int $param
168     * @return int
169     */
170    public function getParamFlags( int $param ): int {
171        if ( isset( $this->paramFlags[$param] ) ) {
172            return $this->paramFlags[$param];
173        }
174        if ( $this->variadicParamIndex !== null && $param >= $this->variadicParamIndex ) {
175            return $this->variadicParamFlags;
176        }
177        return 0;
178    }
179
180    /**
181     * @param int $param
182     * @return bool
183     */
184    public function canOverrideNonVariadicParam( int $param ): bool {
185        return ( ( $this->paramFlags[$param] ?? 0 ) & SecurityCheckPlugin::NO_OVERRIDE ) === 0;
186    }
187
188    /**
189     * @return Taintedness|null
190     */
191    public function getVariadicParamSinkTaint(): ?Taintedness {
192        return $this->variadicParamSinkTaint;
193    }
194
195    /**
196     * @return PreservedTaintedness|null
197     * @suppress PhanUnreferencedPublicMethod
198     */
199    public function getVariadicParamPreservedTaint(): ?PreservedTaintedness {
200        return $this->variadicParamPreserveTaint;
201    }
202
203    /**
204     * @return int|null
205     */
206    public function getVariadicParamIndex(): ?int {
207        return $this->variadicParamIndex;
208    }
209
210    /**
211     * @return bool
212     */
213    public function canOverrideVariadicParam(): bool {
214        return ( $this->variadicParamFlags & SecurityCheckPlugin::NO_OVERRIDE ) === 0;
215    }
216
217    /**
218     * Get the *keys* of the params for which we have sink data, excluding variadic parameters
219     *
220     * @return int[]
221     */
222    public function getSinkParamKeysNoVariadic(): array {
223        return array_keys( $this->paramSinkTaints );
224    }
225
226    /**
227     * Get the *keys* of the params for which we have preserve data, excluding variadic parameters
228     *
229     * @return int[]
230     */
231    public function getPreserveParamKeysNoVariadic(): array {
232        return array_keys( $this->paramPreserveTaints );
233    }
234
235    /**
236     * Check whether we have preserve taint data for the given param
237     *
238     * @param int $param
239     * @return bool
240     */
241    public function hasParamPreserve( int $param ): bool {
242        if ( isset( $this->paramPreserveTaints[$param] ) ) {
243            return true;
244        }
245        if ( $this->variadicParamIndex !== null && $param >= $this->variadicParamIndex ) {
246            return (bool)$this->variadicParamPreserveTaint;
247        }
248        return false;
249    }
250
251    /**
252     * Merge this object with another. This respects NO_OVERRIDE, since it doesn't touch any element
253     * where it's set. If the overall taint has UNKNOWN, it's cleared if we're setting it now.
254     *
255     * @param self $other
256     * @return self
257     */
258    public function asMergedWith( self $other ): self {
259        $ret = clone $this;
260
261        foreach ( $other->paramSinkTaints as $index => $baseT ) {
262            if ( ( ( $ret->paramFlags[$index] ?? 0 ) & SecurityCheckPlugin::NO_OVERRIDE ) === 0 ) {
263                if ( isset( $ret->paramSinkTaints[$index] ) ) {
264                    $ret->paramSinkTaints[$index] = $ret->paramSinkTaints[$index]->asMergedWith( $baseT );
265                } else {
266                    $ret->paramSinkTaints[$index] = $baseT;
267                }
268                $ret->paramFlags[$index] = ( $ret->paramFlags[$index] ?? 0 ) | ( $other->paramFlags[$index] ?? 0 );
269            }
270        }
271        foreach ( $other->paramPreserveTaints as $index => $baseT ) {
272            if ( ( ( $ret->paramFlags[$index] ?? 0 ) & SecurityCheckPlugin::NO_OVERRIDE ) === 0 ) {
273                if ( isset( $ret->paramPreserveTaints[$index] ) ) {
274                    $ret->paramPreserveTaints[$index] = $ret->paramPreserveTaints[$index]->asMergedWith( $baseT );
275                } else {
276                    $ret->paramPreserveTaints[$index] = $baseT;
277                }
278                $ret->paramFlags[$index] = ( $ret->paramFlags[$index] ?? 0 ) | ( $other->paramFlags[$index] ?? 0 );
279            }
280        }
281
282        if ( ( $ret->variadicParamFlags & SecurityCheckPlugin::NO_OVERRIDE ) === 0 ) {
283            $variadicIndex = $other->variadicParamIndex;
284            if ( $variadicIndex !== null ) {
285                $ret->variadicParamIndex = $variadicIndex;
286                $sinkVariadic = $other->variadicParamSinkTaint;
287                if ( $sinkVariadic ) {
288                    if ( $ret->variadicParamSinkTaint ) {
289                        $ret->variadicParamSinkTaint = $ret->variadicParamSinkTaint->asMergedWith( $sinkVariadic );
290                    } else {
291                        $ret->variadicParamSinkTaint = $sinkVariadic;
292                    }
293                }
294                $presVariadic = $other->variadicParamPreserveTaint;
295                if ( $presVariadic ) {
296                    if ( $ret->variadicParamPreserveTaint ) {
297                        $ret->variadicParamPreserveTaint = $ret->variadicParamPreserveTaint
298                            ->asMergedWith( $presVariadic );
299                    } else {
300                        $ret->variadicParamPreserveTaint = $presVariadic;
301                    }
302                }
303                $ret->variadicParamFlags |= $other->variadicParamFlags;
304            }
305        }
306
307        if ( ( $ret->overallFlags & SecurityCheckPlugin::NO_OVERRIDE ) === 0 ) {
308            // Remove UNKNOWN, which could be added e.g. when building func taint from the return type.
309            $ret->overall = $ret->overall->without( SecurityCheckPlugin::UNKNOWN_TAINT )
310                ->asMergedWith( $other->overall );
311            $ret->overallFlags |= $other->overallFlags;
312        }
313
314        return $ret;
315    }
316
317    public function withoutPreserved(): self {
318        $ret = clone $this;
319        $ret->paramPreserveTaints = [];
320        $ret->variadicParamPreserveTaint = null;
321        return $ret;
322    }
323
324    public function asOnlyPreserved(): self {
325        $ret = new self( Taintedness::safeSingleton() );
326        $ret->paramPreserveTaints = $this->paramPreserveTaints;
327        $ret->variadicParamPreserveTaint = $this->variadicParamPreserveTaint;
328        return $ret;
329    }
330
331    /**
332     * @codeCoverageIgnore
333     */
334    public function toString(): string {
335        $str = "[\n\toverall: " . $this->overall->toShortString() .
336            self::flagsToString( $this->overallFlags ) . ",\n";
337        $parKeys = array_unique( array_merge(
338            array_keys( $this->paramSinkTaints ),
339            array_keys( $this->paramPreserveTaints )
340        ) );
341        foreach ( $parKeys as $par ) {
342            $str .= "\t$par: {";
343            if ( isset( $this->paramSinkTaints[$par] ) ) {
344                $str .= "Sink: " . $this->paramSinkTaints[$par]->toShortString() . ', ';
345            }
346            if ( isset( $this->paramPreserveTaints[$par] ) ) {
347                $str .= "Preserve: " . $this->paramPreserveTaints[$par]->toShortString();
348            }
349            $str .= '} ' . self::flagsToString( $this->paramFlags[$par] ?? 0 ) . ",\n";
350        }
351        if ( $this->variadicParamIndex !== null ) {
352            $str .= "\t...{$this->variadicParamIndex}: {";
353            if ( $this->variadicParamSinkTaint ) {
354                 $str .= "Sink: " . $this->variadicParamSinkTaint->toShortString() . ', ';
355            }
356            if ( $this->variadicParamPreserveTaint ) {
357                $str .= "Preserve: " . $this->variadicParamPreserveTaint->toShortString();
358            }
359            $str .= '} ' . self::flagsToString( $this->variadicParamFlags ) . "\n";
360        }
361        return "$str]";
362    }
363
364    /**
365     * @codeCoverageIgnore
366     */
367    private static function flagsToString( int $flags ): string {
368        $bits = [];
369        if ( $flags & SecurityCheckPlugin::NO_OVERRIDE ) {
370            $bits[] = 'no override';
371        }
372        if ( $flags & SecurityCheckPlugin::ARRAY_OK ) {
373            $bits[] = 'array ok';
374        }
375        return $bits ? ' (' . implode( ', ', $bits ) . ')' : '';
376    }
377
378    /**
379     * @codeCoverageIgnore
380     */
381    public function __toString(): string {
382        return $this->toString();
383    }
384}