Summary
All four frameworks ship the Swagger UI as STATIC assets in their bundled public
directory (swagger/index.html + swagger/oauth2-redirect.html). Static files are
resolved independently of the gated /swagger routes, so with swagger DISABLED
(TINA4_SWAGGER_ENABLED=false, or TINA4_DEBUG falsy, or tina4 serve --production)
the Swagger UI page is still served.
Affected: 3.13.82 and earlier (the assets date back to March).
Impact (deliberately not overstated)
The gated route /swagger/openapi.json IS correctly blocked (404), so the OpenAPI
spec is NOT disclosed. What leaks is the dev UI shell: a Swagger-branded page that
then fails to fetch its own spec. No API structure and no data disclosure. It is
still a dev surface reachable on a production deployment, and it defeats the
documented TINA4_SWAGGER_ENABLED production switch.
Reproduction
tina4 init python demo && cd demo
TINA4_OVERRIDE_CLIENT=true tina4 serve --production --no-browser
curl -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7146/swagger/index.html
Measured before the fix (production, swagger disabled)
| framework |
/swagger |
/swagger/ |
/swagger/index.html |
/swagger/oauth2-redirect.html |
| PHP |
200 |
200 |
200 |
200 |
| Node |
200 |
200 |
200 |
200 |
| Python |
404 |
200 |
200 |
200 |
| Ruby |
404 |
404 |
200 |
200 |
Why it stayed hidden: a bare /swagger already 404s on Python (index resolution only
fires for an empty path or a trailing slash) and on Ruby (the gated handler
intercepts), so testing only the obvious URL looked clean. PHP and Node DO
index-resolve a bare /swagger.
Root cause
Static serving runs BEFORE route matching and applies directory-index resolution, so
public/swagger/index.html answers /swagger regardless of the swagger gate. The
gate logic itself was correct in every framework; it was simply never consulted on
the static path.
Fix
Gate the /swagger* static path on the same predicate that gates the routes, checked
BEFORE index resolution, failing closed when the swagger module cannot load:
- python:
tina4_python/core/server.py (_try_static)
- ruby:
lib/tina4/rack_app.rb (try_static)
- php:
Tina4/StaticFiles.php (tryServe)
- node:
packages/core/src/server.ts (boot-time flag set from swaggerEnabled())
After the fix all four return 404 on every URL above in production, and 200 on all of
them in dev, so the feature itself is unchanged.
Lock-in tests
One per framework, each including a guard that asserts the assets exist on disk (so
the negative test cannot pass vacuously), plus positive, negative, TINA4_DEBUG
fallback, explicit-flag-beats-debug, and a non-swagger control asset. Each was
verified to FAIL against the unfixed code (python 3, ruby 3, php 3, node 5 failures)
and pass with the fix.
Fixed in the working tree, pending release.
Summary
All four frameworks ship the Swagger UI as STATIC assets in their bundled public
directory (
swagger/index.html+swagger/oauth2-redirect.html). Static files areresolved independently of the gated
/swaggerroutes, so with swagger DISABLED(
TINA4_SWAGGER_ENABLED=false, orTINA4_DEBUGfalsy, ortina4 serve --production)the Swagger UI page is still served.
Affected: 3.13.82 and earlier (the assets date back to March).
Impact (deliberately not overstated)
The gated route
/swagger/openapi.jsonIS correctly blocked (404), so the OpenAPIspec is NOT disclosed. What leaks is the dev UI shell: a Swagger-branded page that
then fails to fetch its own spec. No API structure and no data disclosure. It is
still a dev surface reachable on a production deployment, and it defeats the
documented
TINA4_SWAGGER_ENABLEDproduction switch.Reproduction
Measured before the fix (production, swagger disabled)
Why it stayed hidden: a bare
/swaggeralready 404s on Python (index resolution onlyfires for an empty path or a trailing slash) and on Ruby (the gated handler
intercepts), so testing only the obvious URL looked clean. PHP and Node DO
index-resolve a bare
/swagger.Root cause
Static serving runs BEFORE route matching and applies directory-index resolution, so
public/swagger/index.htmlanswers/swaggerregardless of the swagger gate. Thegate logic itself was correct in every framework; it was simply never consulted on
the static path.
Fix
Gate the
/swagger*static path on the same predicate that gates the routes, checkedBEFORE index resolution, failing closed when the swagger module cannot load:
tina4_python/core/server.py(_try_static)lib/tina4/rack_app.rb(try_static)Tina4/StaticFiles.php(tryServe)packages/core/src/server.ts(boot-time flag set fromswaggerEnabled())After the fix all four return 404 on every URL above in production, and 200 on all of
them in dev, so the feature itself is unchanged.
Lock-in tests
One per framework, each including a guard that asserts the assets exist on disk (so
the negative test cannot pass vacuously), plus positive, negative,
TINA4_DEBUGfallback, explicit-flag-beats-debug, and a non-swagger control asset. Each was
verified to FAIL against the unfixed code (python 3, ruby 3, php 3, node 5 failures)
and pass with the fix.
Fixed in the working tree, pending release.