$in_customizer_preview ) ) as $name => $value ) { // Mehrere Content-Security-Policy-Kopfzeilen sind erlaubt und wirken // zusammen -- eine bereits gesetzte Policy eines anderen Plugins // wird so nicht ueberschrieben. header( $name . ': ' . $value, 'Content-Security-Policy' !== $name ); } } /** * Alle Header ausser der CSP, die sich aus einer Kontext-Konfiguration * ergeben. Gemeinsame Quelle fuer den PHP-Versand (apply()) und den * .htaccess-Spiegel (class-htaccess.php) -- beide muessen denselben Wert * liefern, sonst bekaeme ein Besucher je nach Cache-Treffer eine andere * Richtlinie. * * @param array $config Kontext-Konfiguration. * @param array{in_customizer_preview?: bool, is_ssl?: bool} $options Abweichende Laufzeitannahmen; ohne Angabe gilt die aktuelle Anfrage. * * @return array Header-Name => Wert. */ public static function static_header_values( array $config, array $options = array() ) { $in_customizer_preview = ! empty( $options['in_customizer_preview'] ); $is_ssl = array_key_exists( 'is_ssl', $options ) ? (bool) $options['is_ssl'] : is_ssl(); // Durchweg empty()/??-Zugriffe: die Methode wird auch vom // .htaccess-Spiegel aufgerufen und darf an einer unvollstaendigen // Konfiguration nicht mit einem Fatal Error die Seite abschiessen. $headers = array(); if ( ! empty( $config['hsts']['enabled'] ) && $is_ssl ) { $value = 'max-age=' . (int) ( $config['hsts']['max_age'] ?? 0 ); if ( ! empty( $config['hsts']['include_subdomains'] ) ) { $value .= '; includeSubDomains'; } if ( ! empty( $config['hsts']['preload'] ) ) { $value .= '; preload'; } $headers['Strict-Transport-Security'] = $value; } if ( ! empty( $config['x_frame_options']['enabled'] ) && ! $in_customizer_preview ) { $headers['X-Frame-Options'] = $config['x_frame_options']['value'] ?? 'SAMEORIGIN'; } // Eigene, erzwungene Kopfzeile mit nur dieser Direktive. Neben einer // Report-Only-Haupt-CSP bleibt sie bestehen (andere Kopfzeile); eine // scharf geschaltete Haupt-CSP ersetzt sie und fuehrt die Direktive // selbst mit (siehe apply() und class-htaccess.php). $frame_ancestors = self::frame_ancestors_value( $config ); if ( '' !== $frame_ancestors && ! $in_customizer_preview ) { $headers['Content-Security-Policy'] = 'frame-ancestors ' . $frame_ancestors; } if ( ! empty( $config['x_content_type_options']['enabled'] ) ) { $headers['X-Content-Type-Options'] = 'nosniff'; } if ( ! empty( $config['referrer_policy']['enabled'] ) && ! empty( $config['referrer_policy']['value'] ) ) { $headers['Referrer-Policy'] = $config['referrer_policy']['value']; } if ( ! empty( $config['permissions_policy']['enabled'] ) && trim( (string) ( $config['permissions_policy']['value'] ?? '' ) ) !== '' ) { $headers['Permissions-Policy'] = $config['permissions_policy']['value']; } if ( ! empty( $config['coop']['enabled'] ) && ! empty( $config['coop']['value'] ) ) { $headers['Cross-Origin-Opener-Policy'] = $config['coop']['value']; } if ( ! empty( $config['corp']['enabled'] ) && ! empty( $config['corp']['value'] ) ) { $headers['Cross-Origin-Resource-Policy'] = $config['corp']['value']; } return $headers; } /** * Opt-in (Standard AUS): ein falsch erkanntes is_ssl() hinter einem * Reverse-Proxy/CDN, der SSL vor dem Server terminiert, koennte sonst * eine Redirect-Schleife verursachen. Respektiert deshalb zusaetzlich * X-Forwarded-Proto -- meldet ein vorgelagerter Proxy hier bereits * "https", wird NICHT umgeleitet, selbst wenn is_ssl() lokal false ist. */ private static function maybe_redirect_to_https() { if ( is_ssl() ) { return; } if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) ) ) { return; } if ( ( defined( 'WP_CLI' ) && WP_CLI ) || wp_doing_cron() ) { return; } // Nur Seitenabrufe umleiten: ein POST wuerde bei 301 zu GET und verlore // seine Daten. $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtolower( sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'get'; if ( ! in_array( $method, array( 'get', 'head' ), true ) ) { return; } // Host (und Port) kommen aus der Website-Adresse, der Pfad samt Query aus // der Anfrage -- unveraendert, damit kodierte Zeichen (Umlaute) erhalten // bleiben und ein Unterverzeichnis nicht doppelt auftaucht. $home = wp_parse_url( home_url() ); $host = isset( $home['host'] ) ? $home['host'] : ''; if ( '' === $host ) { return; } $port = isset( $home['port'] ) ? ':' . (int) $home['port'] : ''; $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '/'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- wird unten bereinigt und per esc_url_raw geprueft. $request_uri = '/' . ltrim( (string) preg_replace( '/[\x00-\x20\x7f]/', '', (string) $request_uri ), '/' ); $target = esc_url_raw( 'https://' . $host . $port . $request_uri ); wp_safe_redirect( $target, 301 ); exit; } /** * Quellenliste fuer frame-ancestors oder '' wenn ausgeschaltet. Nur die * beiden in den Einstellungen erlaubten Werte kommen durch -- ein * manipulierter Optionswert landet so nie in einer Kopfzeile. * * @param array $config Kontext-Konfiguration. */ public static function frame_ancestors_value( array $config ) { if ( empty( $config['frame_ancestors']['enabled'] ) ) { return ''; } $value = $config['frame_ancestors']['value'] ?? "'self'"; return in_array( $value, array( "'self'", "'none'" ), true ) ? $value : "'self'"; } }