While working with a client, one of the most respected construction companies in the U.S., Syndicode was helping deliver a custom internal tool built on Ruby on Rails and ActiveAdmin. Everything was on track until a quiet failure brought key workflows to a stop.
Dynamic forms stopped responding. Admins couldn’t create or update records. Support tickets started coming in. And the frontend gave no clear indication of what went wrong.
The cause? A missing CSP nonce—a subtle security misconfiguration that blocked inline JavaScript without breaking the UI visually.
In this post, we’ll break down how we diagnosed the issue, why it matters for security and functionality, and how we implemented a fix that restored full behavior without compromising CSP rules. Whether you’re working with Rails, ActiveAdmin, or any secure-by-default stack, this one’s worth bookmarking.
A silent failure that broke key admin workflows
Everything looked fine on the surface. No visual bugs, no alerts, no errors in the deployment pipeline. But users began reporting that certain admin forms, the ones they relied on daily, had stopped working.
- Fields no longer showed or hid dynamically
- Form validations didn’t trigger
- Buttons stayed disabled no matter what users selected
And yet, the frontend didn’t throw any visible errors.
The only clue was tucked away in the browser console:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'"
Because the console message only shows a simplified view of the violation, we checked the network response headers to see what was actually being enforced. Sure enough, the Content Security Policy header included:
script-src 'self'
No hash, no nonce, no exceptions. The browser was blocking all inline JavaScript code due to a CSP inline script violation.
From there, we dug deeper to validate what scripts were being blocked and why.
- Inspecting the page’s HTML source confirmed that ActiveAdmin was still rendering small inline scripts using script do … end blocks, which generate a script element directly in the view, powering dynamic behavior in the forms. These script elements controlled dynamic form behavior but were being silently ignored by the browser due to CSP restrictions.
- Next, we looked at the Rails CSP configuration. At the time, the app was set up with a strict script-src ‘self’ policy in the application config file, but hadn’t yet integrated nonce support. This is a common state for teams tightening browser-side security: the policy is technically safe, but operationally incomplete.
- The result was predictable and invisible. A security layer meant to protect the app ended up disabling legitimate functionality. And because the UI didn’t visibly break, the issue slipped past automated checks.
This also highlighted the need to strengthen our test coverage for web pages that depend on inline script execution, especially in environments with strict CSP settings. We’ve since added targeted checks to flag silent failures like this early, even when the UI appears visually correct.
Why Content Security Policy breaks inline JavaScript
Modern browsers enforce Content Security Policy (CSP) rules to prevent malicious code from executing in the browser, especially code injected via cross-site scripting (XSS). It’s a critical layer of defense, but if misconfigured, it can just as easily block legitimate behavior.
In our case, the CSP directive in the response headers was:
script-src 'self'
That tells the user’s browser: only allow JavaScript from the site itself and block all inline scripts by default. Inline JavaScript is considered risky because it can be easily exploited if your app ever becomes vulnerable to XSS.
The issue was that ActiveAdmin relied on inline scripts to drive form interactivity: small blocks rendered inline via script do … end. These aren’t dangerous in this context, because they’re generated by Rails itself. But to the browser, there’s no way to know that unless we explicitly tell it.
That’s where nonces come in.
A nonce is a random string generated for each request, added to both the CSP header and each inline <script> tag. This creates a secure handshake: if the nonce in the header matches the one on the script, the browser executes it. If not, it blocks it.
In our Ruby on Rails app, we had a CSP configuration in place, but it didn’t generate a nonce value and didn’t allow for hashes or any other fallback. So the browser did exactly what it was supposed to: it blocked every CSP inline script, including the ones ActiveAdmin needed to function.
This was a textbook case of strong security rules without proper implementation, resulting in a silently broken UI.
Solutions we considered
Once we confirmed that the issue was caused by a strict Content Security Policy, we evaluated several possible solutions. Each came with trade-offs in terms of security, maintainability, and development effort.
Option A: Move all JavaScript to external files
Pros:
- Most secure, avoids inline scripts entirely
- Keeps CSP simple and strict
Cons:
- High dev effort: ActiveAdmin wasn’t designed for full JS separation
- Refactoring dozens of small inline snippets into external files would’ve been overkill for our use case
Verdict: Overly rigid. Clean in theory, not practical for a tool like ActiveAdmin.
Option B: Add ‘unsafe-inline’ to CSP
Pros:
- Everything “just works” again; zero changes needed to the scripts
- Quickest short-term fix
Cons:
- Major security risk as it re-enables the exact behavior CSP is meant to prevent
- Leaves the application open to XSS vulnerabilities
Verdict: Rejected immediately. It defeats the purpose of using CSP in the first place.
Option C: Use CSP hashes for each inline script
Pros:
- Secure and compliant with CSP
- Allows specific inline scripts to run
Cons:
- Brittle: hashes break any time the script changes (even whitespace)
- Hard to maintain in dynamic environments like ActiveAdmin, where scripts are rendered per request
Verdict: Too fragile and hard to automate for our use case.
Option D: Use a CSP nonce (final choice)
Pros:
- Secure: only server-rendered scripts with a valid nonce are allowed
- Minimal changes to existing code
- Ideal for use with Rails and ActiveAdmin
- Works well with dynamic content and multiple inline scripts per page
Cons:
- Requires generating and injecting a nonce per request
Verdict: The best balance of security, maintainability, and developer convenience.
Choosing a nonce-based approach allowed us to enable CSP while preserving all essential admin panel functionality. It was the right call for both the short and long term.
Need expert help with your Rails?
Whether you’re dealing with security edge cases like CSP, scaling admin interfaces, or building complex features, Syndicode offers senior-level Ruby on Rails development services grounded in real-world experience.
Explore our Rails expertiseImplementing CSP nonces in Rails + ActiveAdmin
Once we aligned on using nonces as the right solution, the implementation was straightforward with some precautions taken to maintain security and compatibility across the app.
Here’s the step-by-step breakdown of how we applied the fix in a Rails app using ActiveAdmin:
1. Update the CSP configuration in Rails
We modified the existing initializer at config/initializers/content_security_policy.rb to generate a random nonce value per request and attach it to the CSP response header.
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
# Add other directives as needed
end
# Generate a new nonce for each request
config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
# Apply the nonce to script-src directives
config.content_security_policy_nonce_directives = %w[script-src]
end
2. Expose the nonce to ActiveAdmin views
ActiveAdmin needs access to the nonce so it can apply it to inline scripts. We exposed it through the controller layer:
controller do
helper_method :csp_nonce
private
def csp_nonce
content_security_policy_nonce
end
end
This makes the nonce available in the view context where the form is defined.
3. Add the nonce to inline script blocks
Anywhere ActiveAdmin uses inline scripts, typically within form do … end blocks, we attached the nonce:
form do |f|
# ... form fields ...
script nonce: csp_nonce do
raw <<~JAVASCRIPT
document.addEventListener("DOMContentLoaded", function() {
const select = document.querySelector('#select_field');
if (select) {
select.addEventListener('change', function() {
// dynamic form logic
});
}
});
JAVASCRIPT
end
end
This ensures that the script is only executed if it carries the correct nonce generated for that request.
4. Recompile assets and restart the server
To ensure the updated policy and scripts took effect:
rake assets:precompile
rails server
5. Verify the fix
We confirmed that:
- The CSP header now included Content-Security-Policy: script-src ‘self’ ‘nonce-xyz…’
- Inline scripts were rendered with matching nonce=”xyz…” attributes
- Dynamic form behavior in ActiveAdmin was fully restored
- CSP remained strict, with no fallback to unsafe-inline or weakened rules
Dynamic scripts ran securely. Admin workflows worked again. And we maintained a clean, secure CSP implementation the way it should be.
Takeaways for the engineering team
What looked like a minor UI issue turned out to be a security misalignment with real operational consequences, the kind of edge case that’s easy to miss until it impacts end users.
We had already started rolling out stricter CSP settings as part of a broader effort to harden the application against XSS and other browser-based attacks. The script-src ‘self’ directive was a deliberate choice that worked for our purposes, but like many security upgrades, it introduced side effects we hadn’t fully accounted for at the time.
To address this holistically, we made a few key improvements:
- Added lightweight end-to-end tests to catch CSP-related regressions in dynamic forms.
- Updated our internal security checklist to ensure any future CSP changes include QA validation for inline JS behavior.
- Documented a standard approach to nonce-based CSP implementation in Rails, especially for teams using ActiveAdmin or similar tools.
None of these changes is heavy-handed, but they reflect a broader principle we follow:
Treat security as part of the product, not just the platform.
For teams like the one we supported here, who rely on internal tools for daily operations, this approach ensures performance and protection go hand in hand.
Conclusion
The Content Security Policy misalignment blocking essential inline JavaScript is a reminder that browser-level security features are powerful, but they don’t exist in isolation. They need to be treated as part of the product experience, not just the infrastructure.
What made the difference here wasn’t just identifying the problem; it was understanding how different factors, from user agent behavior to server-side policies, impacted script execution. The Syndicode team connected the dots quickly and implemented a solution that aligned security with functionality.
That’s the kind of work we aim for: not just patching issues, but improving systems along the way.
If you ever suspect the same problem, here’s what to watch for:
If you’re working with Ruby on Rails, ActiveAdmin, or any app that uses inline JavaScript, keep an eye out for:
- Dynamic form fields stop responding without visible errors
- Buttons stay disabled even with valid input
- Client-side logic silently fails
- No JavaScript errors, just a console warning like:
Refused to execute inline script because it violates the following Content Security Policy directive: “script-src ‘self'”
Spotting these signs early can save valuable time and prevent downstream disruptions in the tools your teams rely on.
If you’re looking to refine or secure your internal tools, Syndicode offers development and consulting services backed by real-world experience.
Frequently asked questions
-
Does using a CSP nonce weaken security in any way?
No. When implemented correctly, nonce values allow trusted inline scripts to run without exposing the app to XSS risks. It’s a standard, secure practice in modern web applications.
-
Is CSP blocking inline JavaScript specific to ActiveAdmin, or could it affect other admin panels or web apps?
While this example focused on ActiveAdmin, any framework that relies on inline JavaScript can run into similar problems under strict CSP rules.
-
Can this type of CSP-related JavaScript failure be caught through regular QA processes?
Not always. Silent failures caused by security headers often bypass visual checks or automated test coverage. They require specific test cases and environment configuration, including: / Verifying that CSP headers are correctly configured / Confirming script execution through browser console warnings / Testing behavior under production-like security settings After encountering this issue, we at Syndicode strengthened our QA workflows to include targeted checks for this class of problem, particularly for admin tools or components that rely on inline JavaScript.
-
What’s the cost/effort to implement CSP nonces in an existing Rails project?
For most teams, it’s a manageable effort, especially compared to the cost of broken features in production. The article outlines a step-by-step path that’s easy to adopt.
-
How do I generate and apply a CSP nonce in Rails 7?
Rails 7 supports CSP nonces natively. You can use config.content_security_policy_nonce_generator and expose the nonce in views via content_security_policy_nonce.
-
Will this approach work with Stimulus or Turbo in Hotwire-based apps?
Yes, but the integration may vary. You’ll need to ensure any inline controllers or JavaScript logic also include the correct nonce or are moved to external files.
-
Can I use hashes instead of nonces for inline scripts?
Technically, yes, but hashes are brittle: they break anytime the script changes (even whitespace). For dynamic scripts, nonces are much more practical.
-
What happens if a browser doesn’t support CSP nonces?
Most modern browser versions fully support nonce-based CSP. For older browsers, fallback behavior depends on how the policy is configured.