Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/running_psalm/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ When `true`, Psalm will report class and interface methods that override a metho
loadLibraryStubs="[bool]"
>
```
When `true`, Psalm loads the stubs it ships for the libraries your code uses (in `stubs/libraries`), which declare how their methods pass taints along. A stub is only loaded when the scan finds one of the classes it declares, and it only refines what it declares of them. Defaults to `true`.
When `true`, Psalm loads the stubs it ships for the libraries your code uses (in `stubs/libraries`), which declare how their methods pass taints along and what they may do (their [capabilities](../annotating_code/purity_model.md)). A stub is only loaded when the scan finds one of the classes it declares, or the scanned code (e.g. the files your autoloader includes) declares one of its functions, and it only refines what it declares of them. Defaults to `true`.

#### phpVersion
```xml
Expand Down
44 changes: 29 additions & 15 deletions src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use LogicException;
use OutOfBoundsException;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Function_;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
Expand Down Expand Up @@ -64,6 +65,7 @@
use XdgBaseDir\Xdg;
use stdClass;

use function array_any;
use function array_key_exists;
use function array_keys;
use function array_merge;
Expand Down Expand Up @@ -448,7 +450,8 @@ final class Config
public ?string $library_stubs_dir = null;

/**
* @var array<string, array<string, list<string>>> the library stubs of each directory, with their class-likes
* @var array<string, array<string, array{list<string>, list<lowercase-string>}>> the library stubs of each
* directory, with their class-likes and functions
*/
private static array $library_stubs = [];

Expand Down Expand Up @@ -2630,24 +2633,27 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null):

$dir_lvl_2 = dirname(__DIR__, 2);

// the stubs of the libraries the scanned code uses: those declaring a class-like the scan found
// the stubs of the libraries the scanned code uses: those declaring a class-like the scan found, or a
// function the scanned code declares (a library of functions only is loaded through its autoloaded files)
$library_stubs = [];
$available_library_stubs = $this->load_library_stubs
? self::getLibraryStubs(
$this->library_stubs_dir
?? $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'libraries',
)
: [];
foreach ($available_library_stubs as $library_stub => $classlikes) {
foreach ($classlikes as $classlike) {
if ($codebase->classlike_storage_provider->has($classlike)) {
$library_stubs[] = $library_stub;
// the real definitions of the others, which the stub then refines like a class the scan found
foreach ($classlikes as $other_classlike) {
$codebase->scanner->queueClassLikeForScanning($other_classlike, false, false);
}

break;
foreach ($available_library_stubs as $library_stub => [$classlikes, $functions]) {
if (array_any(
$classlikes,
static fn(string $classlike): bool => $codebase->classlike_storage_provider->has($classlike),
) || array_any(
$functions,
static fn(string $function): bool => $codebase->functions->isDeclaredInCode($function),
)) {
$library_stubs[] = $library_stub;
// the real definitions of the class-likes, which the stub then refines like a class the scan found
foreach ($classlikes as $classlike) {
$codebase->scanner->queueClassLikeForScanning($classlike, false, false);
}
}
}
Expand Down Expand Up @@ -2760,9 +2766,10 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null):
}

/**
* The library stubs of $dir, `<vendor>/<package>.phpstub`, with the class-likes each of them declares
* The library stubs of $dir, `<vendor>/<package>.phpstub`, with the class-likes and the functions each of them
* declares
*
* @return array<string, list<string>>
* @return array<string, array{list<string>, list<lowercase-string>}>
*/
private static function getLibraryStubs(string $dir): array
{
Expand Down Expand Up @@ -2799,7 +2806,14 @@ private static function getLibraryStubs(string $dir): array
}
}

$library_stubs[$path] = $classlikes;
$functions = [];
foreach ((new NodeFinder())->findInstanceOf($stmts, Function_::class) as $function) {
if ($function->namespacedName !== null) {
$functions[] = strtolower($function->namespacedName->toString());
}
}

$library_stubs[$path] = [$classlikes, $functions];
}
}

Expand Down
21 changes: 21 additions & 0 deletions stubs/libraries/amphp/amp.phpstub
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

// The capabilities (see docs/annotating_code/purity_model.md) of amphp/amp 3. Loaded when the analysed code uses the
// package (see Config::visitStubFiles()).

namespace Amp {

/**
* Non-blocking sleep for the specified number of seconds: it suspends the current fiber and runs the event loop
* meanwhile, as a blocking sleep lets other processes run, so it does io as usleep() does.
*
* @param float $timeout Number of seconds to wait.
* @param bool $reference If false, unreference the underlying watcher.
* @param Cancellation|null $cancellation Cancel waiting if cancellation is requested.
*
* @psalm-capabilities io
*/
function delay(float $timeout, bool $reference = true, ?Cancellation $cancellation = null): void
{
}
}
38 changes: 38 additions & 0 deletions stubs/libraries/amphp/byte-stream.phpstub
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

// The taints amphp/byte-stream 2 passes along: what a payload's methods return is the content of the payload, so
// a payload a source taints (e.g. the body of an amphp/http-client response) taints it. Loaded when the analysed
// code uses the package, refining its classes (see Config::visitStubFiles()).

namespace Amp\ByteStream {

use Amp\Cancellation;

final class Payload implements ReadableStream, \IteratorAggregate, \Stringable
{
/**
* @psalm-flow ($this) -> return
*/
final public function read(?Cancellation $cancellation = null): ?string
{
}

/**
* Buffers entire stream before returning. Use {@see self::read()} to read the stream chunk by chunk.
*
* @throws BufferException|StreamException
*
* @psalm-flow ($this) -> return
*/
final public function buffer(?Cancellation $cancellation = null, int $limit = \PHP_INT_MAX): string
{
}

/**
* @psalm-flow ($this) -> return
*/
public function __toString(): string
{
}
}
}
123 changes: 123 additions & 0 deletions stubs/libraries/amphp/file.phpstub
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

// The taints amphp/file 3 passes along: the paths its functions open, read, write, list, move, link, delete or change
// are file sinks. Loaded when the analysed code uses the package's functions (see Config::visitStubFiles()).

namespace Amp\File {

/**
* @throws FilesystemException
*
* @psalm-taint-sink file $path
*/
function openFile(string $path, string $mode): File
{
}

/**
* @psalm-taint-sink file $original
* @psalm-taint-sink file $link
*/
function createSymlink(string $original, string $link): void
{
}

/**
* @psalm-taint-sink file $target
* @psalm-taint-sink file $link
*/
function createHardlink(string $target, string $link): void
{
}

/**
* @psalm-taint-sink file $from
* @psalm-taint-sink file $to
*/
function move(string $from, string $to): void
{
}

/**
* @psalm-taint-sink file $path
*/
function deleteFile(string $path): void
{
}

/**
* @psalm-taint-sink file $path
*/
function createDirectory(string $path, int $mode = 0777): void
{
}

/**
* @psalm-taint-sink file $path
*/
function createDirectoryRecursively(string $path, int $mode = 0777): void
{
}

/**
* @psalm-taint-sink file $path
*/
function deleteDirectory(string $path): void
{
}

/**
* @return list<string>
*
* @psalm-taint-sink file $path
*/
function listFiles(string $path): array
{
}

/**
* @psalm-taint-sink file $path
*/
function changePermissions(string $path, int $mode): void
{
}

/**
* @param int|null $uid null to ignore
* @param int|null $gid null to ignore
*
* @psalm-taint-sink file $path
*/
function changeOwner(string $path, ?int $uid, ?int $gid = null): void
{
}

/**
* @param int|null $modificationTime The touch time. If $time is not supplied, the current system time is used.
* @param int|null $accessTime The access time. If not supplied, the modification time is used.
*
* @psalm-taint-sink file $path
*/
function touch(string $path, ?int $modificationTime = null, ?int $accessTime = null): void
{
}

/**
* @param string $path The file path from which to buffer contents.
*
* @psalm-taint-sink file $path
*/
function read(string $path): string
{
}

/**
* @param string $path The file path to which to $contents should be written.
* @param string $contents The data to write to the specified $path.
*
* @psalm-taint-sink file $path
*/
function write(string $path, string $contents): void
{
}
}
106 changes: 106 additions & 0 deletions stubs/libraries/amphp/http-client.phpstub
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

// The taints amphp/http-client 5 passes along: the URI a request is sent to is an SSRF sink, the path of a file a
// form uploads a file sink, and what the server sends back, the headers and the body of a response, an input source.
// Loaded when the analysed code uses the package, refining its classes (see Config::visitStubFiles()).
//
// The sources are declared on Response itself rather than on the amphp/http and amphp/byte-stream classes it inherits
// from or returns, which also hold the messages and streams the code builds: the body is the payload getBody()
// returns, whose content Payload's methods return (see byte-stream.phpstub).

namespace Amp\Http\Client {

use Amp\ByteStream\Payload;
use Amp\Http\HttpMessage;
use Amp\Http\HttpRequest;
use Amp\Http\HttpResponse;
use Psr\Http\Message\UriInterface;

/**
* @psalm-import-type HeaderParamValueType from HttpMessage
* @psalm-import-type HeaderParamArrayType from HttpMessage
* @psalm-type ProtocolVersion = '1.0'|'1.1'|'2'
*/
final class Request extends HttpRequest
{
/**
* @param non-empty-string $method
*
* @psalm-taint-sink ssrf $uri
*/
public function __construct(UriInterface|string $uri, string $method = "GET", HttpContent|string $body = '')
{
}

/**
* @psalm-taint-sink ssrf $uri
*/
public function setUri(UriInterface|string $uri): void
{
}
}

final class Form implements HttpContent
{
/**
* @param string $path Local file path. Filename will be provided to the server.
* @throws HttpException
*
* @psalm-taint-sink file $path
*/
public function addFile(string $name, string $path, ?string $contentType = null): void
{
}
}

/**
* @psalm-import-type HeaderParamValueType from HttpMessage
* @psalm-import-type HeaderParamArrayType from HttpMessage
* @psalm-type ProtocolVersion = '1.0'|'1.1'|'2'
*/
final class Response extends HttpResponse
{
/**
* Returns the headers as a string-indexed array of arrays of strings or an empty array if no headers
* have been set.
*
* @return array<non-empty-string, list<string>>
*
* @psalm-taint-source input
*/
public function getHeaders(): array
{
}

/**
* Returns the array of values for the given header or an empty array if the header does not exist.
*
* @return list<string>
*
* @psalm-taint-source input
*/
public function getHeaderArray(string $name): array
{
}

/**
* Returns the value of the given header. If multiple headers are present for the named header, only the first
* header value will be returned. Use getHeaderArray() to return an array of all values for the particular header.
* Returns null if the header does not exist.
*
* @psalm-taint-source input
*/
public function getHeader(string $name): ?string
{
}

/**
* Retrieve the response body.
*
* @psalm-taint-source input
*/
public function getBody(): Payload
{
}
}
}
Loading
Loading