Content-Security-Policy in a Complex Environment
Content-Security-Policy (CSP) is a web security mechanism that helps protect against various types of cybersecurity attacks by defining and enforcing a set of policies regarding the content that a website can load and subsequently execute.
Essentially, it’s an allow-list policy that dictates what a web page can load. CSP is complex to implement and rollout - even a minor mistake could mean that important parts of the page will not load, which in Okta’s case could mean trouble authenticating. This blog article aims to provide a glimpse into our secure implementation journey and guidance for the industry based on lessons learned.
Okta values web security
Okta employs various defenses against Cross-Site Scripting (XSS) attacks such as input validation and output encoding to increase security assurance against emerging threats. With MITRE and CISA’s confirmation of XSS as 2024’s top threat, it’s highly probable that an application is vulnerable to XSS at some point in time. Content-Security-Policy is effectively a gate-keeper that dictates to the browser which sources of scripts and content are secure, trusted, and can be executed. Okta’s environment is complex, and as such, our CSP header is constantly being improved upon.
Implementation challenges
A key pillar of the Okta Secure Identity Commitment (OSIC) includes raising the bar for the industry - here, we’re sharing our industry learnings, tips, tricks, and more. Upon configuration of CSP policies at Okta, the Engineering Security team encountered the following challenges throughout the implementation.
Complexity
The nature of Okta Workforce Identity is that it operates in an environment with multiple application connections, varying feature combinations, and html pages that are customizable by Okta administrators. It becomes evident quickly that building and rolling out even a basic Content-Security-Policy can be challenging.
Configuration challenges
The most prominent determination in CSP configuration is whether or not the application in question returns endpoints that contain customizable content by Okta administrators. The following three detailed approaches include our recommendations in configuring CSP:
1. The interceptor approach
A common approach is to use an interceptor to add the CSP headers. At the time, this best fit our model at Okta, so this is where we started. But there are some challenges here, such as:
An addition of correct policy for endpoints that return user customized html content,
Caution with the interceptor order: the CSP headers need to be added as close to the beginning of the order of interceptors, in case some interceptors break the interceptor chain early.
The preHandle() and postHandle() are not always known if the content-type is html, but can be indicated by using annotations at the endpoint level to determine the response type,
Commitment of response via XHR in the preHandle(), which cannot be modified as the postHandle() is executed,
Database calls in an interceptor may be cached, and the time response has been committed afterCompletion().
2. The filter approach
An alternate approach to adding a CSP header when the application returns html content is to apply the header when the content type of the endpoint is known. The following example displays a method of CSP generation as a filter in a spring web application, based on the content-type header that is returned to only apply the CSP for html:
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(response) {
@Override
public void addHeader(String name, String value) {
super.addHeader(name, value);
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setCSPHeaders(value);
}
}
@Override
public void setHeader(String name, String value) {
super.setHeader(name, value);
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setCSPHeaders(value);
}
}
@Override
public void setContentType(String type) {
super.setContentType(type);
setCSPHeaders(type);
}
private void setCSPHeaders(String contentType) {
if (StringUtils.isNotEmpty(contentType) && StringUtils.containsIgnoreCase(type, MimeTypeUtils.TEXT_HTML_VALUE) {
LOG.debug("Content-Type header={}", contentType)
// the code to build the Content-Security-Policy-Report-Only and Content-Security-Policy headers
}
}
};
filterChain.doFilter(request, wrapper);
}
3. The edge approach
Scott Helme details this approach in his blog post, where he leverages the edge to intercept HTTP traffic and inject the CSP policy. In his particular example, he uses the Cloudflare Service workers, as an “easy” way to implement the generic CSP policy. One advantage of this method is that it can be applied to multiple applications, which gives you a single maintenance point for maintaining the CSP policy. Though generic, this approach can be added to any application without changing its code.
Configuration considerations
Violation reports
Okta’s application range is quite large and has many endpoints, which resulted in too many violation reports. The reporting URI to which CSP tells the browser to forward the violation reports will quickly produce a large amount of data. And, it’s mostly the same violations repeated multiple times per page load. The reporting vendor may have a way to ignore certain violations that are expected since they should not be in the policy. Reporting vendors aren’t incentivized to ignore features, since they receive the reports and have to process them. Okta’s method in tackling this problem was to implement a sampling of violation reports on the server side where the CSP headers are added. We provided knobs to control the requests that will receive the CSP headers, ultimately reducing the traffic sent to our reporting endpoint. An alternative to this method could be to build an in-house solution to receive the reporting data and dismiss repeated violations at the receiving end.
Sample violation: Where Content-Security-Policy (CSP) did not intercept:
{
"csp-report": {
"effective-directive": "connect-src",
"original-policy": [truncated]
"blocked-uri": "https://mail.google.com/mail/feed/atom/",
"source-file": "user-script",
"line-number": 5,
"column-number": 16842
}
}
Sample violation: Where Content-Security-Policy (CSP) intercepted:
Content-Security-Policy securely blocks known malicious scripts such as this one that was reported to us by report-uri. The example below illustrates a truncated excerpt CSP interception, adding to the environment’s security assurance posture:
{
"csp-report": {
"effective-directive": "script-src-elem",
"Original-policy": [truncated]
"blocked-uri": "https://3001.scriptcdn.net/code/static/1",
"line-number": 7,
"column-number": 47,
"status-code": 200
}
}
Internal forward requests
A request may go through the interceptor chain several times when forwarded internally. This can create complications if CSP headers are computed each time, especially if the computed header changes or is removed, since it cannot be unset once set. Complications arise when a page that is customizable by administrators is forwarded to a page that is not customizable or the other way around, as each page requires a different CSP. In our use case, we rolled out a base policy containing frame-ancestors which was used as a way to revert an incorrectly-computed policy due to forwards.
Testing
In order to set a specific policy for a specific customer to perform thorough live testing and debugging, we leverage Java Management Extensions (JMX) because it gives us the ability to modify the CSP policy live in the application while on a discovery call with customers.
Selenium tests are considered to be of great importance - without these tests, something is likely to break in production when one least expects it. We built a framework that allows us to fail a selenium test if a CSP error was present in the browser console of a selenium test.
Customized content
Okta’s admin console user interface (UI) allows the admin to customize the Content-Security-Policy (CSP) for customizable pages. This encourages the creation of CSP that allows their customizations to execute and toggle that policy between Content-Security-Policy-Report-Only and Content-Security-Policy. Also, they have the option to provide their own reporting URL for browser-based violation reports.
Directives
Navigational directives such as “frame-ancestors” should always be added to prevent a malicious actor from attempting to iFrame not only html content, but also APIs. We recommend fetch directives only for endpoints that return html content, considering the downside of non-html content causing an increase in network traffic due to larger header size in the response.
Header size limitations
API gateways such as AWS API, Google Cloud Apigee API and Kong API Gateway all have limitations on the response header size ranging from 4KB to 128KB. With the introduction of nonces to tackle unsafe-inline, the response header size can surpass 4KB. Special consideration must be taken with customers using API gateways to allow for a higher response size.
Rollout Challenges
In an effort to share our lessons learned, the following subsections capture rollout challenges encountered throughout each of the three above-listed configuration methods:
Unsafe-eval
Tackling unsafe-eval is the first step in putting a stop to new code that is not templated. The next step is to track each existing violation and to remove all the exemptions from the linting allow-list. The last step is to remove the unsafe-eval keyword from the policy.
Unsafe-inline
Expect a significant impact if unsafe-inline is removed from the policy. One key risk in the removal is a high probability of user impact if the policy is incorrect. Impacts could include blocking an inline-script or inline-style on a page which creates a bad user experience (UX) or even cause a page not to load properly. For third-party integrations that require unsafe-inline for inline-script or inline-style, it’s best to request the vendor to fix their code to not require unsafe-inline, otherwise, you’re stuck with inherited poor practices. If an integration is required such as Pendo or Mapbox, it may take time for the vendor to implement a fix which removes the unsafe-inline/unsafe-eval requirement.
The approach we preferred was to empower each team to rollout their endpoints by using annotations which control whether adding a nonce to script-src and style-src for both Content-Security-Policy-Report-Only and Content-Security-Policy. The following CSP example can be untimely but assures a lower risk in testing independently:
@RequestMapping(value = “/api/v1/object, method = RequestMethod.GET)
@ScriptSrcNonce(policy = {ScriptSrcNoncePolicy.SCRIPT_SRC_NONCE_REPORT_ONLY, ScriptSrcNoncePolicy.SCRIPT_SRC_NONCE_ENFORCED}, switchProperty = "team.<name>.<endpoint>kill.switch.enableScriptSrcNonce")
public String listObjectProperties(ModelMap model) {
…
}
Recommendations
We recommend the use of feature flags to control various parts of the CSP, and cloud configuration knobs for added control during deployment. When rolling out, we recommend a slower pace with guardrails such as enabling Continuous Integration (CI), using development environments, performing live testing with specific customer configurations using JMX as mentioned above, and lastly, in production environments. When monitoring, we recommend incrementally adjusting the policy and repeating as needed. As in most rollout plans, focus on the largest customer impact on the initial rollout in order to deploy a policy, then improve the policy over time, working closely with customers to debug issues as they arise.
Lastly, we’d recommend being prepared to remediate by rolling back the CSP to a stable state. Knobs such as feature flags and cloud configurations, as mentioned above, are very important in rolling back to get you reverted to a working, functional state. CSP’s can be evaluated in real time for continuous improvement.
Conclusion
In the end, is implementing Content-Security-Policy worth all the effort? From our security teams to yours, yes!
The top security vulnerability, being Cross-Site Scripting (XSS) in modern web applications, is combatted at the framework-level by a strong CSP. They’re deployed for stronger, added security in preparation for the long haul against today’s evolving threats. Due to its importance, we’re seeing an increase in customer requests for custom domains. Content-Security-Policy continues to be a security priority at Okta with continued security investigation, enhancement and monitoring in an effort to secure customer data.