You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
helmet() is mounted globally, applying its default Content-Security-Policy to every response — including the Swagger UI docs page mounted at /api-docs in development — with no CSP exception configured for that route. swagger-ui-express (the package serving that page) renders by injecting inline <script>/<style> content, which helmet's default CSP directives (script-src 'self', style-src 'self', no 'unsafe-inline') block by design.
// src/routes/apiDocs.jsif(config.nodeEnv==='development'){router.use('/',swaggerUi.serve,swaggerUi.setup(openApiDocument,{explorer: true,customSiteTitle: 'SmartDrop API Docs',}));}else{router.get('/',(_req,res)=>{res.redirect('/api-docs/openapi.yaml');});}
helmet() is called with no options — meaning helmet 8.x's default contentSecurityPolicy middleware is active, applying its default directive set (which does not permit 'unsafe-inline' for scripts or styles) to every response the app sends, with no per-route override anywhere in src/index.js or src/routes/apiDocs.js disabling or relaxing it for the /api-docs path specifically. swagger-ui-express's swaggerUi.setup(...) middleware serves an HTML page that bootstraps the Swagger UI React application via inline <script> tags (and the UI itself injects further inline styles at runtime) — content a strict default CSP with no 'unsafe-inline' allowance for script-src/style-src blocks browsers from executing/applying, per browsers' standard CSP enforcement behavior. The practical result: visiting /api-docs in a browser that respects CSP would load the HTML shell successfully (helmet doesn't block the page from loading, only specific resource types it disallows) but the interactive Swagger UI itself would fail to initialize — console CSP violation errors, a blank or non-functional docs explorer — defeating the purpose of swaggerUi.explorer: true and the whole /api-docs feature, in the one environment (development) where it's actually served as an interactive UI rather than redirected to the raw YAML.
This is a well-known, commonly-documented interaction (searching "helmet swagger-ui-express CSP" surfaces this exact gotcha broadly across the ecosystem) — the standard fix is either disabling contentSecurityPolicy specifically for the docs route, or explicitly configuring script-src/style-src directives to permit what swagger-ui-express needs (ideally via nonces rather than a blanket 'unsafe-inline', to avoid weakening CSP protection for the rest of the app).
Requirements
Configure a CSP exception for the /api-docs route specifically — either a separate helmet({ contentSecurityPolicy: false }) (or an equivalently scoped override) applied only to that router, or explicit directives permitting what swagger-ui-express needs, applied only there — rather than weakening the global CSP for the entire application.
Prefer the narrowest fix that restores Swagger UI functionality without broadly disabling CSP protection app-wide; if 'unsafe-inline' is unavoidable for swagger-ui-express's bundled assets, scope it to the /api-docs path only.
Verify manually (or via a headless-browser test) that the docs page actually renders and is interactive after the fix, not just that the HTTP response succeeds — a 200 status alone would not have caught this bug, since the failure is purely in browser-side CSP enforcement of the response headers, not in the HTTP response itself.
Acceptance Criteria
/api-docs in NODE_ENV=development renders a fully interactive Swagger UI in a real (or headless) browser with no CSP violation errors in the console.
The rest of the application's routes retain helmet's default (or an equivalently strict) CSP — the fix is scoped to /api-docs only, not a global CSP relaxation.
A test (ideally using a headless browser like Puppeteer/Playwright if available in this project's test tooling, or at minimum an assertion on the actual Content-Security-Policy response header value for /api-docs versus other routes) verifies the scoped exception is in place.
test/api-docs.test.js is extended to cover this, since its current coverage (per the file's existence) evidently doesn't catch a CSP-only failure mode (an HTTP-level test asserting 200 and a body containing expected HTML would pass today even though the page doesn't actually work in a real browser).
Additional Notes
More precise references
src/index.js: confirmed app.use(helmet()); is called with no arguments (default configuration) and is mounted before any router, applying to all subsequent responses including /api-docs.
src/index.js: confirmed app.use('/api-docs', globalApiLimit); app.use('/api-docs', apiDocsRouter); — no helmet override, no CSP-specific middleware, anywhere between the global helmet() call and the docs router.
src/routes/apiDocs.js:15-19: confirmed swaggerUi.serve, swaggerUi.setup(openApiDocument, { explorer: true, ... }) is only mounted when config.nodeEnv === 'development' — the production path (else branch, lines 20-23) just redirects to the raw openapi.yaml file and would not be affected by this bug at all, since it never serves the Swagger UI HTML/JS.
helmet^8.2.0 (per package.json): confirmed current helmet major versions enable contentSecurityPolicy by default as part of the bundled middleware set, with a default directive list that does not include 'unsafe-inline' for script-src/style-src.
Additional edge cases
Since this only manifests in NODE_ENV=development (production redirects instead of serving the interactive UI), the practical blast radius is limited to local development and any shared "dev"/staging environment where /api-docs is used interactively — but that's presumably exactly the use case the explorer: true interactive Swagger UI was added for in the first place (per closed issue Add OpenAPI 3.0 specification for all API endpoints #26, "Add OpenAPI 3.0 specification for all API endpoints"), so it's a real, currently-broken developer-experience feature, not a cosmetic edge case.
Worth double-checking whether helmet's other default protections (e.g. X-Frame-Options, Cross-Origin-Embedder-Policy) also interfere with Swagger UI's iframe/resource-loading behavior in some configurations — the CSP issue is the most certain and most commonly documented one, but a full manual/browser check during the fix should look for any other console warnings, not just CSP ones.
Test/reproduction plan
Start the app with NODE_ENV=development, request /api-docs with a real or headless browser, and check the browser console for CSP violation errors (Refused to execute inline script because it violates the following Content Security Policy directive...) — this would not be visible from a plain supertest/HTTP-level test, only from something that actually parses and enforces the returned headers against the returned body, which is why this has likely gone unnoticed by test/api-docs.test.js if that test only asserts HTTP-level status/body-contains-string assertions.
After the fix, confirm the Content-Security-Policy response header for GET /api-docs differs from (is more permissive, in a scoped way) than the header for e.g. GET /api/v1/prices/XLM, proving the exception is scoped rather than global.
Add CORS policy hardening and configurable allowed origins #32 (closed) — "Add CORS policy hardening and configurable allowed origins" — different middleware, same general "security headers/policy middleware interacting with a specific route's needs" neighborhood; worth a shared glance during review in case any similar route-specific exception is also needed for CORS on /api-docs, though nothing found during this review suggests CORS is currently broken there.
Overview
helmet()is mounted globally, applying its default Content-Security-Policy to every response — including the Swagger UI docs page mounted at/api-docsin development — with no CSP exception configured for that route.swagger-ui-express(the package serving that page) renders by injecting inline<script>/<style>content, which helmet's default CSP directives (script-src 'self',style-src 'self', no'unsafe-inline') block by design.helmet()is called with no options — meaninghelmet8.x's defaultcontentSecurityPolicymiddleware is active, applying its default directive set (which does not permit'unsafe-inline'for scripts or styles) to every response the app sends, with no per-route override anywhere insrc/index.jsorsrc/routes/apiDocs.jsdisabling or relaxing it for the/api-docspath specifically.swagger-ui-express'sswaggerUi.setup(...)middleware serves an HTML page that bootstraps the Swagger UI React application via inline<script>tags (and the UI itself injects further inline styles at runtime) — content a strict default CSP with no'unsafe-inline'allowance forscript-src/style-srcblocks browsers from executing/applying, per browsers' standard CSP enforcement behavior. The practical result: visiting/api-docsin a browser that respects CSP would load the HTML shell successfully (helmet doesn't block the page from loading, only specific resource types it disallows) but the interactive Swagger UI itself would fail to initialize — console CSP violation errors, a blank or non-functional docs explorer — defeating the purpose ofswaggerUi.explorer: trueand the whole/api-docsfeature, in the one environment (development) where it's actually served as an interactive UI rather than redirected to the raw YAML.This is a well-known, commonly-documented interaction (searching "helmet swagger-ui-express CSP" surfaces this exact gotcha broadly across the ecosystem) — the standard fix is either disabling
contentSecurityPolicyspecifically for the docs route, or explicitly configuringscript-src/style-srcdirectives to permit whatswagger-ui-expressneeds (ideally via nonces rather than a blanket'unsafe-inline', to avoid weakening CSP protection for the rest of the app).Requirements
/api-docsroute specifically — either a separatehelmet({ contentSecurityPolicy: false })(or an equivalently scoped override) applied only to that router, or explicitdirectivespermitting whatswagger-ui-expressneeds, applied only there — rather than weakening the global CSP for the entire application.'unsafe-inline'is unavoidable forswagger-ui-express's bundled assets, scope it to the/api-docspath only.200status alone would not have caught this bug, since the failure is purely in browser-side CSP enforcement of the response headers, not in the HTTP response itself.Acceptance Criteria
/api-docsinNODE_ENV=developmentrenders a fully interactive Swagger UI in a real (or headless) browser with no CSP violation errors in the console./api-docsonly, not a global CSP relaxation.Content-Security-Policyresponse header value for/api-docsversus other routes) verifies the scoped exception is in place.test/api-docs.test.jsis extended to cover this, since its current coverage (per the file's existence) evidently doesn't catch a CSP-only failure mode (an HTTP-level test asserting200and a body containing expected HTML would pass today even though the page doesn't actually work in a real browser).Additional Notes
More precise references
src/index.js: confirmedapp.use(helmet());is called with no arguments (default configuration) and is mounted before any router, applying to all subsequent responses including/api-docs.src/index.js: confirmedapp.use('/api-docs', globalApiLimit); app.use('/api-docs', apiDocsRouter);— no helmet override, no CSP-specific middleware, anywhere between the globalhelmet()call and the docs router.src/routes/apiDocs.js:15-19: confirmedswaggerUi.serve, swaggerUi.setup(openApiDocument, { explorer: true, ... })is only mounted whenconfig.nodeEnv === 'development'— the production path (elsebranch, lines 20-23) just redirects to the rawopenapi.yamlfile and would not be affected by this bug at all, since it never serves the Swagger UI HTML/JS.helmet^8.2.0(perpackage.json): confirmed current helmet major versions enablecontentSecurityPolicyby default as part of the bundled middleware set, with a default directive list that does not include'unsafe-inline'forscript-src/style-src.Additional edge cases
NODE_ENV=development(production redirects instead of serving the interactive UI), the practical blast radius is limited to local development and any shared "dev"/staging environment where/api-docsis used interactively — but that's presumably exactly the use case theexplorer: trueinteractive Swagger UI was added for in the first place (per closed issue Add OpenAPI 3.0 specification for all API endpoints #26, "Add OpenAPI 3.0 specification for all API endpoints"), so it's a real, currently-broken developer-experience feature, not a cosmetic edge case.helmet's other default protections (e.g.X-Frame-Options,Cross-Origin-Embedder-Policy) also interfere with Swagger UI's iframe/resource-loading behavior in some configurations — the CSP issue is the most certain and most commonly documented one, but a full manual/browser check during the fix should look for any other console warnings, not just CSP ones.Test/reproduction plan
NODE_ENV=development, request/api-docswith a real or headless browser, and check the browser console for CSP violation errors (Refused to execute inline script because it violates the following Content Security Policy directive...) — this would not be visible from a plainsupertest/HTTP-level test, only from something that actually parses and enforces the returned headers against the returned body, which is why this has likely gone unnoticed bytest/api-docs.test.jsif that test only asserts HTTP-level status/body-contains-string assertions.Content-Security-Policyresponse header forGET /api-docsdiffers from (is more permissive, in a scoped way) than the header for e.g.GET /api/v1/prices/XLM, proving the exception is scoped rather than global.Cross-references
/api-docs, though nothing found during this review suggests CORS is currently broken there.