4096 || str_contains( $path, "\0" ) || str_contains( $path, '\\' ) || str_contains( $path, ':' ) || str_starts_with( $path, '/' ) || preg_match( '~(^|/)(\.|\.\.)(/|$)~', $path ) || str_contains( $path, '//' ) ) { throw new \RuntimeException( 'Unsafe relative path.' ); } } public static function beneath( string $path, string $root ): bool { $root = rtrim( $root, '/' ); return $path === $root || str_starts_with( $path, $root . '/' ); } public static function noLinks( string $path ): void { if ( ! str_starts_with( $path, '/' ) ) { throw new \RuntimeException( 'Absolute local POSIX path required.' ); } $part = ''; foreach ( explode( '/', trim( $path, '/' ) ) as $segment ) { if ( '' === $segment || '.' === $segment || '..' === $segment ) { throw new \RuntimeException( 'Noncanonical path.' ); } $part .= '/' . $segment; clearstatcache( true, $part ); $st = @lstat( $part ); if ( ! $st || 0120000 === ( $st['mode'] & 0170000 ) ) { throw new \RuntimeException( 'Missing or symlinked path.' ); } } } public static function resolve( string $root, string $relative, bool $writable = false ): string { self::relative( $relative ); $root = rtrim( $root, '/' ); self::noLinks( $root ); if ( realpath( $root ) !== $root ) { throw new \RuntimeException( 'Noncanonical scan root.' ); } $path = $root . '/' . $relative; self::noLinks( $path ); $real = realpath( $path ); if ( $real !== $path || ! self::beneath( $real, $root ) ) { throw new \RuntimeException( 'Path containment failed.' ); } $st = lstat( $real ); $type = $st['mode'] & 0170000; if ( ! in_array( $type, array( 0100000, 0040000 ), true ) || ( 0100000 === $type && 1 !== $st['nlink'] ) ) { throw new \RuntimeException( 'Special files and hardlinks are unsupported.' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Native inode, permission and handle checks enforce the local repair boundary. if ( $writable && ( ! is_file( $real ) || ! is_writable( dirname( $real ) ) || ( $st['mode'] & 0022 ) ) ) { throw new \RuntimeException( 'Repair requires a regular file and protected writable parent.' ); } return $real; } public static function read( string $root, string $relative, int $maxBytes = 2097152 ): array { $path = self::resolve( $root, $relative ); $before = lstat( $path ); if ( ! is_file( $path ) || ! is_readable( $path ) || $before['size'] > $maxBytes ) { throw new \RuntimeException( 'Unreadable or oversized file; not inspected.' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Native inode, permission and handle checks enforce the local repair boundary. $stream = @fopen( $path, 'rb' ); if ( ! $stream ) { throw new \RuntimeException( 'Could not read file.' ); } try { $opened = fstat( $stream ); if ( $opened['ino'] !== $before['ino'] || $opened['dev'] !== $before['dev'] || 1 !== $opened['nlink'] ) { throw new \RuntimeException( 'File changed before read.' ); } $content = stream_get_contents( $stream, $maxBytes + 1 ); if ( ! is_string( $content ) || strlen( $content ) > $maxBytes ) { throw new \RuntimeException( 'File exceeds scan budget.' ); } clearstatcache( true, $path ); $after = lstat( $path ); if ( ! $after || $after['ino'] !== $opened['ino'] || $after['size'] !== strlen( $content ) || $after['mtime'] !== $opened['mtime'] || $after['ctime'] !== $opened['ctime'] ) { throw new \RuntimeException( 'File changed during read; rescan required.' ); } self::resolve( $root, $relative ); return array( 'content' => $content, 'hash' => hash( 'sha256', $content ), 'stat' => $after, 'path' => $path, ); } finally { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Native inode, permission and handle checks enforce the local repair boundary. fclose( $stream ); } } public static function archiveEntry( string $name, int $size, int $compressed, int $mode = 0, int $maxEntryBytes = 8388608 ): void { self::relative( rtrim( $name, '/' ) ); if ( $size < 0 || $size > min( 16777216, $maxEntryBytes ) || ( $size > 1048576 && ( $compressed <= 0 || $size / max( 1, $compressed ) > 200 ) ) ) { throw new \RuntimeException( 'Archive size or expansion budget exceeded.' ); } $type = $mode & 0170000; if ( $type && ! in_array( $type, array( 0100000, 0040000 ), true ) ) { throw new \RuntimeException( 'Archive links and special entries are refused.' ); } } }