@@ -49,16 +49,17 @@ public class ContentSecurityPolicyFilter implements Filter
4949
5050 private static final String NONCE_SUBST = "REQUEST.SCRIPT.NONCE" ;
5151 private static final String REPORT_PARAMETER_SUBSTITUTION = "CSP.REPORT.PARAMS" ;
52+ private static final String UPGRADE_INSECURE_REQUESTS_SUBSTITUTION = "UPGRADE.INSECURE.REQUESTS" ;
5253 private static final String HEADER_NONCE = "org.labkey.filters.ContentSecurityPolicyFilter#NONCE" ; // needs to match PageConfig.HEADER_NONCE
5354
5455 private static final Map <ContentSecurityPolicyType , ContentSecurityPolicyFilter > CSP_FILTERS = new CopyOnWriteHashMap <>();
5556
5657 // Lock that protects the static data structures below
57- private static final Object ALLOWED_SOURCES_LOCK = new Object ();
58+ private static final Object SUBSTITUTION_LOCK = new Object ();
5859 private static final Map <Directive , SetValuedMap <String , String >> ALLOWED_SOURCES = new HashMap <>();
5960 // Regenerate and stash on every "allowed source" change as a convenience (so every filter doesn't need to recalculate
6061 // it on every init() and change)
61- private static Map <String , String > ALLOWED_SOURCES_SUBSTITUTION_MAP = Collections .emptyMap ();
62+ private static Map <String , String > SUBSTITUTION_MAP = Collections .emptyMap ();
6263
6364 // Per-filter-instance parameters that are set in init() and never changed
6465 private ContentSecurityPolicyType _type = ContentSecurityPolicyType .Enforce ;
@@ -89,7 +90,7 @@ public String getHeaderName()
8990
9091 static
9192 {
92- // ReactJS hot reload uses localhost port 3001. If in dev mode, allow browser to access that port for fonts
93+ // ReactJS hot reload uses localhost port 3001. If in dev mode, allow the browser to access that port for fonts
9394 // and connections.
9495 if (AppProps .getInstance ().isDevMode ())
9596 {
@@ -203,10 +204,10 @@ private void regeneratePolicyExpression()
203204 {
204205 final String allowSubstitutedPolicy ;
205206
206- synchronized (ALLOWED_SOURCES_LOCK )
207+ synchronized (SUBSTITUTION_LOCK )
207208 {
208209 allowSubstitutedPolicy = StringExpressionFactory .create (_policyTemplate , false , NullValueBehavior .KeepSubstitution )
209- .eval (ALLOWED_SOURCES_SUBSTITUTION_MAP );
210+ .eval (SUBSTITUTION_MAP );
210211 }
211212
212213 _policyExpression = StringExpressionFactory .create (allowSubstitutedPolicy , false , NullValueBehavior .ReplaceNullAndMissingWithBlank );
@@ -250,7 +251,7 @@ public static void registerAllowedSources(Directive directive, String key, Strin
250251
251252 public static void registerAllowedSources (String key , Directive directive , String ... allowedSources )
252253 {
253- synchronized (ALLOWED_SOURCES_LOCK )
254+ synchronized (SUBSTITUTION_LOCK )
254255 {
255256 if (allowedSources .length == 0 )
256257 throw new IllegalStateException ("Registering no sources is not allowed" );
@@ -263,7 +264,7 @@ public static void registerAllowedSources(String key, Directive directive, Strin
263264
264265 public static void unregisterAllowedSources (Directive directive , String key )
265266 {
266- synchronized (ALLOWED_SOURCES_LOCK )
267+ synchronized (SUBSTITUTION_LOCK )
267268 {
268269 LOG .debug ("Unregistering {} for {}" , directive , key );
269270 SetValuedMap <String , String > multiMap = ALLOWED_SOURCES .get (directive );
@@ -278,11 +279,11 @@ public static void unregisterAllowedSources(Directive directive, String key)
278279 }
279280
280281 // Regenerate the substitution map and all policy expressions on every register/unregister
281- private static void regenerateSubstitutionMap ()
282+ public static void regenerateSubstitutionMap ()
282283 {
283- synchronized (ALLOWED_SOURCES_LOCK )
284+ synchronized (SUBSTITUTION_LOCK )
284285 {
285- ALLOWED_SOURCES_SUBSTITUTION_MAP = ALLOWED_SOURCES .entrySet ().stream ()
286+ SUBSTITUTION_MAP = ALLOWED_SOURCES .entrySet ().stream ()
286287 .filter (e -> !e .getValue ().isEmpty ())
287288 .collect (Collectors .toMap (
288289 e -> e .getKey ().getSubstitutionKey (),
@@ -294,12 +295,9 @@ private static void regenerateSubstitutionMap()
294295 // Add an empty substitution for sources that lack registrations. This strips them from the stashed policy,
295296 // meaning less work on every request.
296297 Arrays .stream (Directive .values ())
297- .forEach (dir -> ALLOWED_SOURCES_SUBSTITUTION_MAP .putIfAbsent (dir .getSubstitutionKey (), "" ));
298+ .forEach (dir -> SUBSTITUTION_MAP .putIfAbsent (dir .getSubstitutionKey (), "" ));
298299
299- // Backward compatibility for CSPs using old substitution key
300- // TODO: Remove in 25.4 and adjust the junit test below
301- if (ALLOWED_SOURCES_SUBSTITUTION_MAP .containsKey (Directive .Connection .getSubstitutionKey ()))
302- ALLOWED_SOURCES_SUBSTITUTION_MAP .put ("LABKEY.ALLOWED.CONNECTIONS" , ALLOWED_SOURCES_SUBSTITUTION_MAP .get (Directive .Connection .getSubstitutionKey ()));
300+ SUBSTITUTION_MAP .put (UPGRADE_INSECURE_REQUESTS_SUBSTITUTION , AppProps .getInstance ().isSSLRequired () ? "upgrade-insecure-requests;" : "" );
303301
304302 // Tell each registered ContentSecurityPolicyFilter to refresh its policy template based on the new substitution map
305303 CSP_FILTERS .values ().forEach (ContentSecurityPolicyFilter ::regeneratePolicyExpression );
@@ -378,13 +376,13 @@ public void testPolicyFiltering()
378376 @ Test
379377 public void testSubstitutionMap ()
380378 {
381- synchronized (ALLOWED_SOURCES_LOCK )
379+ synchronized (SUBSTITUTION_LOCK )
382380 {
383- // Ensure substitution map has been initialized, otherwise the finally block asserts will fail
381+ // Ensure the substitution map has been initialized; otherwise the finally block asserts will fail
384382 regenerateSubstitutionMap ();
385383 // Make a deep copy of ALLOWED_SOURCES so we can restore it after testing
386384 int sourceMapSize = ALLOWED_SOURCES .size ();
387- int substitutionMapSize = ALLOWED_SOURCES_SUBSTITUTION_MAP .size ();
385+ int substitutionMapSize = SUBSTITUTION_MAP .size ();
388386 Map <Directive , SetValuedMap <String , String >> savedSources = ALLOWED_SOURCES .entrySet ().stream ()
389387 .collect (Collectors .toMap (Map .Entry ::getKey , e -> new HashSetValuedHashMap <>(e .getValue ())));
390388
@@ -408,53 +406,53 @@ public void testSubstitutionMap()
408406 verifySubstitutionMapSize (0 );
409407 registerAllowedSources ("foo" , Directive .Connection , "MySource" );
410408 assertEquals (1 , ALLOWED_SOURCES .size ());
411- verifySubstitutionMapSize (2 ); // Old connection substitution key should be added as well
409+ verifySubstitutionMapSize (1 );
412410 verifySubstitutionInPolicyExpressions ("MySource" , 1 );
413411 registerAllowedSources ("bar" , Directive .Connection , "MySource" );
414412 assertEquals (1 , ALLOWED_SOURCES .size ());
415- verifySubstitutionMapSize (2 );
413+ verifySubstitutionMapSize (1 );
416414 verifySubstitutionInPolicyExpressions ("MySource" , 1 ); // Duplicate source should be filtered out
417415
418416 unregisterAllowedSources (Directive .Font , "font" );
419417 registerAllowedSources ("font" , Directive .Font , "MySource" );
420418 assertEquals (2 , ALLOWED_SOURCES .size ());
421- verifySubstitutionMapSize (3 );
419+ verifySubstitutionMapSize (2 );
422420 verifySubstitutionInPolicyExpressions ("MySource" , 2 );
423421 registerAllowedSources ("font2" , Directive .Font , "MyFontSource" );
424422 assertEquals (2 , ALLOWED_SOURCES .size ());
425- verifySubstitutionMapSize (3 );
423+ verifySubstitutionMapSize (2 );
426424 verifySubstitutionInPolicyExpressions ("MySource" , 2 );
427425 verifySubstitutionInPolicyExpressions ("MyFontSource" , 1 );
428426 unregisterAllowedSources (Directive .Font , "font2" );
429427 assertEquals (2 , ALLOWED_SOURCES .size ());
430- verifySubstitutionMapSize (3 );
428+ verifySubstitutionMapSize (2 );
431429 verifySubstitutionInPolicyExpressions ("MySource" , 2 );
432430 verifySubstitutionInPolicyExpressions ("MyFontSource" , 0 );
433431 unregisterAllowedSources (Directive .Font , "font" );
434432 assertEquals (2 , ALLOWED_SOURCES .size ()); // Font entry still exists, but should be empty
435433 assertTrue (ALLOWED_SOURCES .get (Directive .Font ).isEmpty ());
436- verifySubstitutionMapSize (2 );// Back to the way it was
434+ verifySubstitutionMapSize (1 );// Back to the way it was
437435 verifySubstitutionInPolicyExpressions ("MySource" , 1 );
438436 verifySubstitutionInPolicyExpressions ("MyFontSource" , 0 );
439437
440438 unregisterAllowedSources (Directive .Frame , "frame" );
441439 registerAllowedSources ("frame" , Directive .Frame , "FrameSource" , "FrameStore" );
442440 assertEquals (3 , ALLOWED_SOURCES .size ());
443- verifySubstitutionMapSize (3 );
441+ verifySubstitutionMapSize (2 );
444442 verifySubstitutionInPolicyExpressions ("FrameSource" , 1 );
445443 verifySubstitutionInPolicyExpressions ("FrameStore" , 1 );
446444
447445 unregisterAllowedSources (Directive .Style , "style" );
448446 registerAllowedSources ("style" , Directive .Style , "StyleSource" , "MoreStylishStore" );
449447 assertEquals (4 , ALLOWED_SOURCES .size ());
450- verifySubstitutionMapSize (4 );
448+ verifySubstitutionMapSize (3 );
451449 verifySubstitutionInPolicyExpressions ("StyleSource" , 1 );
452450 verifySubstitutionInPolicyExpressions ("MoreStylishStore" , 1 );
453451
454452 unregisterAllowedSources (Directive .Image , "image" );
455453 registerAllowedSources ("image" , Directive .Image , "ImageSource" , "BetterImageStore" );
456454 assertEquals (5 , ALLOWED_SOURCES .size ());
457- verifySubstitutionMapSize (5 );
455+ verifySubstitutionMapSize (4 );
458456 verifySubstitutionInPolicyExpressions ("ImageSource" , 1 );
459457 verifySubstitutionInPolicyExpressions ("BetterImageStore" , 1 );
460458 }
@@ -465,7 +463,7 @@ public void testSubstitutionMap()
465463 ALLOWED_SOURCES .putAll (savedSources );
466464 regenerateSubstitutionMap ();
467465 assertEquals (sourceMapSize , ALLOWED_SOURCES .size ());
468- assertEquals (substitutionMapSize , ALLOWED_SOURCES_SUBSTITUTION_MAP .size ());
466+ assertEquals (substitutionMapSize , SUBSTITUTION_MAP .size ());
469467 }
470468 }
471469 }
@@ -485,11 +483,13 @@ private void verifySubstitutionInPolicyExpressions(String value, int expectedCou
485483
486484 private void verifySubstitutionMapSize (long expectedNonEmptyValues )
487485 {
488- // Actual map size should stay static throughout test
489- int expectedSubstitutionMapSize = Directive .values ().length + 1 ; // One extra for old "connections" key
486+ // Actual map size should stay static throughout the test
487+ int expectedSubstitutionMapSize = Directive .values ().length + 1 ; // One extra for UPGRADE.INSECURE.REQUESTS
488+ if (AppProps .getInstance ().isSSLRequired ())
489+ expectedNonEmptyValues ++;
490490
491- assertEquals (expectedSubstitutionMapSize , ALLOWED_SOURCES_SUBSTITUTION_MAP .size ());
492- long nonEmptyValues = ALLOWED_SOURCES_SUBSTITUTION_MAP .entrySet ().stream ().filter (e -> !e .getValue ().isEmpty ()).count ();
491+ assertEquals (expectedSubstitutionMapSize , SUBSTITUTION_MAP .size ());
492+ long nonEmptyValues = SUBSTITUTION_MAP .entrySet ().stream ().filter (e -> !e .getValue ().isEmpty ()).count ();
493493 assertEquals (expectedNonEmptyValues , nonEmptyValues );
494494 }
495495 }
0 commit comments