4096 ) { throw new \RuntimeException( 'Invalid server document root metadata.' ); } $documentRoot = sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ); // An exclusion boundary must never be silently rewritten or dropped. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is only the size/unchanged-byte guard around the immediately sanitized document-root exclusion boundary; changed metadata is refused, never accepted as a storage path. if ( $documentRoot !== wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) || str_contains( $documentRoot, "\0" ) ) { throw new \RuntimeException( 'Invalid server document root metadata.' ); } if ( '' !== $documentRoot ) { if ( '/' !== $documentRoot[0] || ! is_dir( $documentRoot ) || false === realpath( $documentRoot ) ) { throw new \RuntimeException( 'Server document root must identify an existing absolute directory.' ); } $roots[] = rtrim( $documentRoot, '/' ) ?: '/'; } } if ( function_exists( 'wp_upload_dir' ) ) { $uploads = wp_upload_dir( null, false ); if ( ! empty( $uploads['basedir'] ) ) { $roots[] = rtrim( $uploads['basedir'], '/' ); } } foreach ( $roots as $root ) { // WordPress content/upload roots may themselves be symlink aliases. Compare // their real locations too, before putting any evidence or packages here. $resolved = realpath( $root ); foreach ( array_unique( array( $root, false === $resolved ? $root : $resolved ) ) as $candidate ) { if ( PathGuard::beneath( $dir, $candidate ) || PathGuard::beneath( $candidate, $dir ) ) { throw new \RuntimeException( 'Recovery storage must be separate from every web/WordPress root.' ); } } } if ( ! function_exists( 'posix_geteuid' ) || fileowner( $dir ) !== posix_geteuid() || ! function_exists( 'fsync' ) || ! function_exists( 'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt' ) ) { throw new \RuntimeException( 'Repair requires POSIX ownership, fsync and sodium authenticated encryption.' ); } for ( $p = $dir;'/' !== $p;$p = dirname( $p ) ) { if ( 0 !== ( fileperms( $p ) & 022 ) ) { throw new \RuntimeException( 'Recovery ancestors must not be writable by other accounts.' ); } } return $dir; } protected static function key(): string { $encoded = getenv( 'ANTIVIRUS_AI_RECOVERY_KEY' ); $key = is_string( $encoded ) ? base64_decode( $encoded, true ) : false; if ( ! is_string( $key ) || 32 !== strlen( $key ) ) { throw new \RuntimeException( 'Repair disabled: environment recovery key must be base64 of 32 random bytes.' ); } return $key; } public static function path( string $id ): string { if ( ! preg_match( '/^p_[a-f0-9]{32}$/D', $id ) ) { throw new \RuntimeException( 'Invalid recovery identifier.' ); } return self::directory() . '/' . $id . '.evidence'; } public static function read( string $id, string $expectedHash ): string { $path = self::path( $id ); PathGuard::noLinks( $path ); $st = @lstat( $path ); if ( ! is_array( $st ) || 0600 !== ( $st['mode'] & 0777 ) || 0100000 !== ( $st['mode'] & 0170000 ) || 1 !== $st['nlink'] || $st['size'] > 2097408 || $st['uid'] !== posix_geteuid() ) { throw new \RuntimeException( 'Unsafe recovery object.' ); } $blob = @file_get_contents( $path ); if ( ! is_string( $blob ) || ! str_starts_with( $blob, "AVAI1\n" ) || strlen( $blob ) < 46 ) { throw new \RuntimeException( 'Corrupt recovery object.' ); } $plain = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( substr( $blob, 30 ), $id . '|' . $expectedHash, substr( $blob, 6, 24 ), self::key() ); if ( ! is_string( $plain ) || ! hash_equals( $expectedHash, hash( 'sha256', $plain ) ) ) { throw new \RuntimeException( 'Recovery authentication failed. Preserve evidence and seek controlled recovery assistance.' ); } return $plain; } }