44
55namespace Icinga \Util ;
66
7+ use Icinga \Application \Icinga ;
8+ use Icinga \Application \Logger ;
9+ use Icinga \Security \SecurityException ;
710use Icinga \Web \Response ;
811use Icinga \Web \Window ;
12+ use ipl \I18n \StaticTranslator ;
913use RuntimeException ;
1014
1115use function ipl \Stdlib \get_php_type ;
@@ -51,9 +55,13 @@ public static function addHeader(Response $response): void
5155 throw new RuntimeException ('No nonce set for CSS ' );
5256 }
5357
58+ $ header = "default-src 'self'; style-src 'self' 'nonce- {$ csp ->styleNonce }'; " ;
59+ $ imageSourceWhitelist = Icinga::app ()->getConfig ()->get ("security " , "image_source_whitelist " , "" );
60+ $ header = $ header . Csp::getImageSourceDirective ($ imageSourceWhitelist );
61+
5462 $ response ->setHeader (
5563 'Content-Security-Policy ' ,
56- " script-src 'self'; style-src 'self' 'nonce- $ csp -> styleNonce '; " ,
64+ $ header ,
5765 true
5866 );
5967 }
@@ -79,6 +87,9 @@ public static function createNonce(): void
7987 */
8088 public static function getStyleNonce (): ?string
8189 {
90+ if (Icinga::app ()->isCli ()) {
91+ return null ;
92+ }
8293 return static ::getInstance ()->styleNonce ;
8394 }
8495
@@ -108,4 +119,52 @@ protected static function getInstance(): self
108119
109120 return static ::$ instance ;
110121 }
122+
123+ public static function getImageSourceDirective (string $ whitelist ): string {
124+ $ directive = "img-src 'self' data: " ;
125+
126+ if (empty ($ whitelist )) {
127+ return $ directive . "; " ;
128+ }
129+
130+ foreach (explode (", " , $ whitelist ) as $ domain ) {
131+ try {
132+ $ domain = Csp::validateImageSourceWhitelistItem ($ domain );
133+ $ directive .= " " . $ domain ;
134+ } catch (SecurityException $ e ) {
135+ Logger::error ("Ignoring domain ' $ domain' as it is not valid. $ e " );
136+ }
137+ }
138+
139+ return $ directive . "; " ;
140+ }
141+
142+ /**
143+ * Validates and trims an item that is used as a domain in the img-src directive.
144+ * Will throw an error, if the user tries to whitelist everything (*) or tries
145+ * to inject special characters that might allow to escape and edit the whole csp.
146+ *
147+ * @throws SecurityException
148+ */
149+ public static function validateImageSourceWhitelistItem (string $ item ): string {
150+ $ item = trim ($ item );
151+ // Don't allow general whitelisting of all domains
152+ if ($ item == '* ' ) {
153+ throw new SecurityException (
154+ StaticTranslator::$ instance ->translate ("Whitelisting all domains is not allowed. " )
155+ );
156+ }
157+
158+ // Don't allow special characters that might allow to escape and edit the whole csp.
159+ // Otherwise, e.g. "example.com; script-src *" will allow the user to change other directives.
160+ $ csp_config_regex = "|^\*?[a-zA-Z0-9+._\-:]*$| " ;
161+ if (!preg_match ($ csp_config_regex , $ item )) {
162+ throw new SecurityException (
163+ StaticTranslator::$ instance ->translate ("The following domain is invalid: " )
164+ . $ item
165+ );
166+ }
167+
168+ return $ item ;
169+ }
111170}
0 commit comments