1

I'm trying to hot-reload a change in the content security policy (CSP) of my Spring Boot application, i.e. the user should be able to change it via an admin UI without restarting the server.

The regular approach in Spring Boot is:

@Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) {
        // ... lots more config here...
        http.headers()
            .addHeaderWriter(
                 StaticHeadersWriter(
                     "Content-Security-Policy", 
                     "<some policy string>"
                 )
            )
    } 
}

... but this doesn't allow for reconfiguration once it has been assigned.

Can I make this (re-)configurable at runtime? Reloading the application context is not an option, I need to be able to adapt only this particular setting.

Martin Häusler
  • 6,544
  • 8
  • 39
  • 66

2 Answers2

4

Easy-Peasy, we only need to expose a (n appropriate) HeaderWriter as a bean! ContentSecurityPolicyHeaderWriter looks appropriate & sufficient for us, but we are also free to implement a custom:

private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";

@Bean
public ContentSecurityPolicyHeaderWriter myWriter(
        @Value("${#my.policy.directive:DEFAULT_SRC_SELF_POLICY}") String initalDirectives
) {
  return new ContentSecurityPolicyHeaderWriter(initalDirectives);
}

Then with:

@Autowired
private ContentSecurityPolicyHeaderWriter myHeadersWriter;

@Override
public void configure(HttpSecurity http) throws Exception {
  // ... lots more config here...
  http.headers()
    .addHeaderWriter(myHeadersWriter);
}

..., we can change the header value with these demo controllers:

@GetMapping("/")
public String home() {
  myHeadersWriter.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
  return "header reset!";
}

@GetMapping("/foo")
public String foo() {
  myHeadersWriter.setPolicyDirectives("FOO");
  return "Hello from foo!";
}

@GetMapping("/bar")
public String bar() {
  myHeadersWriter.setPolicyDirectives("BAR");
  return "Hello from bar!";
}

We can test:

@SpringBootTest
@AutoConfigureMockMvc
class DemoApplicationTests {

  @Autowired
  private MockMvc mockMvc;

  @Test
  public void testHome() throws Exception {
    this.mockMvc.perform(get("/"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("header reset!")))
            .andExpect(header().string(CONTENT_SECURITY_POLICY_HEADER, DEFAULT_SRC_SELF_POLICY));
  }

  @Test
  public void testFoo() throws Exception {
    this.mockMvc.perform(get("/foo"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Hello from foo!")))
            .andExpect(header().string(CONTENT_SECURITY_POLICY_HEADER, "FOO"));
  }

  @Test
  public void testBar() throws Exception {
    this.mockMvc.perform(get("/bar"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Hello from bar!")))
            .andExpect(header().string(CONTENT_SECURITY_POLICY_HEADER, "BAR"));
  }
}

... also in browser:

change header browser screenshot

All in one github.(sorry all in main class!:)


Refs: only this

xerx593
  • 12,237
  • 5
  • 33
  • 64
  • 2
    Thank you for this very comprehensive answer! Implementing a HeaderWriter looks like the right place, I'm going to try this :) – Martin Häusler Dec 02 '21 at 16:32
  • Welcome! Thank you for clear question & accept! :-) The tricky part will be "how/when to propagate that change" and how it will work in a (highly) concurrent environment. (feedback welcome!;) – xerx593 Dec 02 '21 at 17:02
  • 1
    Why would you need a custom implementation? I don't see anything other than in the default filter. You could configure one, call the setter and achieve the same result. Create one in an `@Bean` method which get an `@Value` you now set on a field. Non need to create your own implementation just some configuration. – M. Deinum Dec 02 '21 at 17:05
  • of course, @M.Deinum! (bang my head^^...will update post), but can you estimate, how it will work with concurrency?? – xerx593 Dec 02 '21 at 17:13
  • 1
    It won't, as the bean is a singleton the value set will count for all of the incoming requests and threads. – M. Deinum Dec 03 '21 at 07:34
1

The problem with the (my) accepted answer is:

(just for the show case, but:) We modify "singleton scope property" on (every) request!!!

When we add a "stress" test wrapper like this.

( ... wait until all threads finish their work in java ?? -> ExecutorCompletionService, since Java:1.5;)

It badly fails (header has not the "expected" value):

@Test
void testParallel() throws Exception {
  // 200 cycles, with  0 (== #cpu) threads ...
  final StressTester<Void> stressTestHome = new StressTester<>(Void.class, 200, 0, // ... and these (three) jobs (firing requests at our app):
    () -> {
      home(); // here the original tests
      return null;
    },
    () -> {
      foo(); // ... with assertions ...
      return null;
    },
    () -> {
      bar(); // ... moved to private (non Test) methods
      return null;
    }
  );
  stressTestHome.test(); // run it, collect it and:
  stressTestHome.printErrors(System.out);
  assertTrue(stressTestHome.getExceptionList().isEmpty());
}

As in mock as in (full) server mode... ;(;(;(

We will encounter the same problem, when we want to change that header from a "lower scope" (than singleton..so any other scope:) ;(;(;(

If we want singleton scope policy for that header, and only "trigger the reload" (for all subsequent requests), we can stop reading. (answer 1 is ok, as i actually "initially understood" the question & answered:)

But if we want that "per request header" with , we have to pass this test! :)


One possible solution: Method Injection!

So back to our custom HeaderWriter implementation:

package com.example.demo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.header.HeaderWriter;
// abstract!
public abstract class MyContentSecurityPolicyHeaderWriter implements HeaderWriter {
  // ... no state!!!
  public static final String CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";

  public static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";

  @Override // how cool, that there is a HttpServletRequest/-Response "at hand" !?!
  public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
    if (!response.containsHeader(CONTENT_SECURITY_POLICY_HEADER)) {
      // responsible for the header key, but for the value we ask: delegate
      response.setHeader(CONTENT_SECURITY_POLICY_HEADER, policyDelegate().getPolicyDirectives());
    }
  }

  // TLDR xDxD
  protected abstract MyContentSecurityDelegate policyDelegate();
}

Thanks, again!;)

With this tiny (but managed) "context holder":

package com.example.demo;
import lombok.*;

@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class MyContentSecurityDelegate {

  @Getter
  @Setter
  private String policyDirectives;
}

We do this (with , How to create bean using @Bean in spring boot for abstract class):

@Configuration 
class FreakyConfig {

  @Value("${my.policy.directive:DEFAULT_SRC_SELF_POLICY}")
  private String policy;

  @Bean
  @RequestScope // !! (that is suited for our controllers)
  public MyContentSecurityDelegate delegate() {
    return MyContentSecurityDelegate.of(policy);
  }

  @Bean
  public MyContentSecurityPolicyHeaderWriter myWriter() {
    return new MyContentSecurityPolicyHeaderWriter() { // anonymous inner class
      @Override
      protected MyContentSecurityDelegate policyDelegate() {
        return delegate(); // with request scoped delegate.
      }
    };
  }
}

..then our controllers do that (autowire & "talk" to the delegate):

@Autowired // !
private MyContentSecurityDelegate myRequestScopedDelegate;

@GetMapping("/foo")
public String foo() {
  // !!
  myRequestScopedDelegate.setPolicyDirectives("FOO");
  return "Hello from foo!";
}

Then all tests pass! :) pushed to (same)github.


But to achieve the goal: "Write headers request (even thread) specific", we can use any other technique (matching our stack & needs, beyond ):

Mo' Links:

Happy Coding!

xerx593
  • 12,237
  • 5
  • 33
  • 64