Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

9.0 KiB

Follow-up: connect-src reports for /api/logout (404)

PROD CSP reports from 2026-10-13 and 2026-10-14. The browser is on https://pnsps.gld.gov.hk/login and reports https://pnsps.gld.gov.hk/api/logout as a connect-src violation with HTTP 404.

This is not a React route, and it is not a Content-Security-Policy block. The logout API call is reaching something that answers “not found”. Chrome then files a report-only CSP violation for that failed same-origin request.

Related docs:


What the report actually says

Example (same shape on every hit in this batch):

Field Value Meaning
document-uri https://pnsps.gld.gov.hk/login The page that was loaded when the report was sent
referrer usually https://pnsps.gld.gov.hk/dashboard; one hit from https://epaygateway2.gcis.gov.hk/ Where the user was before /login
violated-directive connect-src Chrome classified the failed request under connect-src
blocked-uri https://pnsps.gld.gov.hk/api/logout The request that failed
status-code 404 The server answered. A real CSP block is status 0
disposition report Report-Only. The policy did not enforce a block
Policy connect-src 'self' Same host is already allowed

Do not add anything to connect-src for this. 'self' already covers https://pnsps.gld.gov.hk.


Why this is not a React route

React Router only runs after an HTML document has loaded, and only for the address bar (/login, /dashboard, and the other paths in src/routes/). It never sees axios / fetch calls.

/api/logout is an API call, not a page:

  1. The user is on /dashboard (or returns from the payment gateway).
  2. Logout runs, or a 401 refresh fails.
  3. The app posts to /api/logout, then moves the browser to /login.

Frontend call (PNSPS-frontend-MaterialUI):

  • src/utils/ApiPathConst.js — LOGOUT = "/logout"
  • src/auth/utils.js — production apiPath is /api
  • src/auth/index.js — handleLogoutFunction does axios.post(\${apiPath}${LOGOUT}`, { refreshToken }), which isPOST /api/logout`
  • Callers then navigate('/login'). The axios 401 path also does window.location.reload()

/login is already a React route (src/routes/LoginRoutes.js, path login). The document in the report loaded. The 404 is the POST, not the login page.

Adding a React route for /api/logout or /logout does not receive this POST. The browser sends it to Apache before React runs.


What the backend already does

PNSPS-backend:

  • src/main/resources/application.yml — server.servlet.contextPath: /api
  • JwtAuthenticationController — @PostMapping("/logout"), response 204 No Content
  • SecurityConfig — LOGOUT_URL = "/logout" is on URL_WHITELIST (permitAll), so an expired session can still call it
  • RefreshTokenService.revokePresented — unknown or bad tokens return without an error. A request that hits this method is still 204

So a correct POST /api/logout is 204, including a dummy refresh token. 404 means the request never entered that method.


What to change on Apache

Two rules, in this order. /api must be proxied to Spring. Page paths that are not real files must be served as index.html with status 200.

Step 1. Confirm where /api goes today

On the PROD Apache host, find the vhost for pnsps.gld.gov.hk and note:

  • ProxyPass / ProxyPassReverse for /api
  • Any RewriteRule that sends unknown paths to index.html
  • Any ErrorDocument 404 or FallbackResource

ErrorDocument 404 /index.html is the wrong tool for a React app. The body can be the SPA while the status stays 404. That is the “SPA shell 404” called out in the 2026-08-03 and 2026-09-22 reviews. It also makes a missed API path look like a client route.

Step 2. Keep /api on the backend

/api must be handled before any SPA rewrite, and must not be rewritten to index.html.

ProxyPreserveHost On
ProxyPass        /api http://127.0.0.1:8090/api
ProxyPassReverse /api http://127.0.0.1:8090/api

Use the real upstream (host and port) already used for /api/csp-report and /api/login. Do not invent a new port if PROD is not on 8090.

If a rewrite engine is also on, stop rewriting /api:

RewriteEngine On
RewriteRule ^/api/ - [L]

If /api/logout is rewritten to index.html:

  • the refresh token is never revoked
  • the browser still sees a failed logout call
  • these CSP reports continue

Step 3. Serve React routes as 200 index.html

Apply this only after the /api rule. It covers /login, /dashboard, and the other client paths. Real files (/static/js/..., css, images) stay as files.

RewriteEngine On

# API is the backend, not a React route. Must stay above the fallback.
RewriteRule ^/api/ - [L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]

Remove ErrorDocument 404 /index.html for this vhost if it is what currently loads the SPA. FallbackResource /index.html is acceptable only when /api is excluded and the fallback response is 200.

Step 4. Reload Apache and deploy the backend that contains logout

The 204 handler has to be in the build that PROD is running. A proxy to an old jar that has no POST /logout still returns 404 from Spring.

sudo apachectl configtest
sudo systemctl reload apache2

Use httpd instead of apache2 if that is the service name on this host.

Step 5. Prove the POST hits Spring

From a machine that can reach PROD:

curl -sS -D - -o /dev/null \
  -X POST "https://pnsps.gld.gov.hk/api/logout" \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"not-a-real-token"}'
Status Meaning Next action
204 Handler ran. Bad token is ignored on purpose Apache and the deployed jar are fine. Go to step 6
404 Still not the logout method Response body: Spring JSON means the proxy works but this build has no mapping. HTML (index.html or an Apache error page) means the SPA/error fallback caught /api/logout. Fix step 2, and deploy the backend if the body is Spring’s 404
401 Request reached Spring Security and was not on the whitelist Deploy the build where LOGOUT_URL is in URL_WHITELIST
405 Path exists, method does not match Something sent GET. The app sends POST. Check for a redirect that turned POST into GET
502 / 503 Proxy target down or wrong Fix the ProxyPass upstream

Also check a real page, not only the API:

curl -sS -D - -o /dev/null "https://pnsps.gld.gov.hk/login"
curl -sS -D - -o /dev/null "https://pnsps.gld.gov.hk/dashboard"

Both should be 200 and Content-Type: text/html (the SPA shell). A 404 here means step 3 is not in effect.

Step 6. Prove logout in the browser

  1. Sign in and open /dashboard.
  2. Open devtools → Network. Keep “Preserve log” on.
  3. Use the header logout control.
  4. Expect POST /api/logout → 204, then the document request for /login → 200.
  5. Repeat once by waiting for session expiry (the axios path in src/auth/index.js posts logout, then navigate('/login') and window.location.reload()). Same 204, then /login 200.

The payment-gateway referrer in one report is the same logout POST. The user landed on /login after epaygateway2.gcis.gov.hk; it is not a separate route to allowlist.

Step 7. Watch CSP reports

After the 204s are confirmed, new connect-src hits for blocked-uri https://pnsps.gld.gov.hk/api/logout should stop. Reports already stored are historical.

Leave the CSP header as in csp-apache.conf.md. Do not add origins, and do not drop connect-src 'self'.


What not to change

Idea Why it does not fix this
New React route for /logout or /api/logout The POST never goes through the router. LogoutPage.js exists and is not mounted; mounting it would not see axios.post
Rewrite /api/* to index.html Logout never revokes the refresh token
Allow /api/logout in CSP 'self' already allows it. Status 404 shows the request was sent
Turn off Report-Only to “hide” it Hides the symptom. The POST would still 404

Code map

Piece Location
Logout POST PNSPS-frontend-MaterialUI/src/auth/index.js (handleLogoutFunction)
401 → logout → /login → reload same file, axios response interceptor
Header logout src/layout/MainLayout/Header/index.js (handleLogout)
API path src/utils/ApiPathConst.js, src/auth/utils.js
Login route src/routes/LoginRoutes.js (path: 'login')
POST /logout → 204 PNSPS-backend JwtAuthenticationController
Whitelist SecurityConfig.LOGOUT_URL
Context path /api src/main/resources/application.yml