fix(security): add HSTS header on stage + integration test
Some checks failed
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
Some checks failed
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Web / Build + push Web (push) Has been cancelled
Docker Web / Deploy Web on stage (push) Has been cancelled
Найдено в Sprint 28 security audit: stage отдаёт security-заголовки (CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy и др.), но БЕЗ Strict-Transport-Security. HSTS из ASP.NET Core (Program.cs UseHsts) не срабатывает потому что api за nginx-прокси видит запрос как HTTP (нет ForwardedHeaders middleware'a; nginx X-Forwarded-Proto не дешифруется). Простейший фикс: добавить HSTS в deploy/nginx.conf (web-контейнер). Brower honors HSTS только на HTTPS-ответах — безопасно unconditional. max-age=2592000 (30 дней), без includeSubDomains и без preload — pre-emptive consent, можно безопасно убрать. Когда production stack устаканится и admin.food-market.kz будет подан в hstspreload.org — увеличить до 31536000 + preload + includeSubDomains. Verified: curl -I https://test.admin.food-market.kz/ | grep -i strict > strict-transport-security: max-age=2592000 Integration test 08-security-headers.spec.ts проверяет 7 security- заголовков на главной + на 404 (always-параметр). Cert: 10/10 integration tests passed in 1.3m. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ed140cb819
commit
a80471d0f9
|
|
@ -14,6 +14,14 @@ server {
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
|
||||||
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
||||||
|
|
||||||
|
# Sprint 28: HSTS. Brower honors HSTS only on HTTPS responses, поэтому
|
||||||
|
# безопасно добавлять unconditionally — если клиент пришёл по HTTP,
|
||||||
|
# header игнорируется. Без includeSubDomains и без preload — это
|
||||||
|
# pre-emptive consent: можно безопасно убрать. Когда production stack
|
||||||
|
# устаканится и admin.food-market.kz будет подан в hstspreload.org,
|
||||||
|
# увеличить max-age до 31536000 + добавить preload и includeSubDomains.
|
||||||
|
add_header Strict-Transport-Security "max-age=2592000" always;
|
||||||
|
|
||||||
# Long-running admin imports (MoySklad etc.) read from upstream for tens of
|
# Long-running admin imports (MoySklad etc.) read from upstream for tens of
|
||||||
# minutes. Bump timeouts only on that path so normal API stays snappy.
|
# minutes. Bump timeouts only on that path so normal API stays snappy.
|
||||||
location /api/admin/import/ {
|
location /api/admin/import/ {
|
||||||
|
|
|
||||||
47
tests/integration/08-security-headers.spec.ts
Normal file
47
tests/integration/08-security-headers.spec.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Sprint 28 — security headers verification.
|
||||||
|
*
|
||||||
|
* Поверяет, что ВСЕ нужные security-заголовки выставлены на каждом
|
||||||
|
* ответе SPA, в т.ч. на 4xx/404. Регрессионная защита: если кто-то
|
||||||
|
* случайно уберёт `always` или `add_header` — этот тест поймает.
|
||||||
|
*/
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { baseUrl } from '../regression/factories/api-client.js'
|
||||||
|
|
||||||
|
const REQUIRED_HEADERS = [
|
||||||
|
// Sprint 13.
|
||||||
|
'content-security-policy',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-content-type-options',
|
||||||
|
'referrer-policy',
|
||||||
|
'permissions-policy',
|
||||||
|
'x-permitted-cross-domain-policies',
|
||||||
|
// Sprint 28.
|
||||||
|
'strict-transport-security',
|
||||||
|
]
|
||||||
|
|
||||||
|
test.describe('27.9 security headers', () => {
|
||||||
|
test('все обязательные security-заголовки на главной странице', async () => {
|
||||||
|
const r = await fetch(`${baseUrl}/`)
|
||||||
|
expect(r.status).toBeLessThan(500)
|
||||||
|
const headers = Object.fromEntries(
|
||||||
|
[...r.headers.entries()].map(([k, v]) => [k.toLowerCase(), v]),
|
||||||
|
)
|
||||||
|
for (const h of REQUIRED_HEADERS) {
|
||||||
|
expect(headers[h], `missing header: ${h}`).toBeTruthy()
|
||||||
|
}
|
||||||
|
// HSTS специфика.
|
||||||
|
expect(headers['strict-transport-security']).toMatch(/max-age=\d+/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('заголовки сохраняются на 404 (always-параметр)', async () => {
|
||||||
|
const r = await fetch(`${baseUrl}/this-path-does-not-exist-deliberately`)
|
||||||
|
// Может быть 404 или 200 (SPA fallback) — оба ок.
|
||||||
|
const headers = Object.fromEntries(
|
||||||
|
[...r.headers.entries()].map(([k, v]) => [k.toLowerCase(), v]),
|
||||||
|
)
|
||||||
|
for (const h of REQUIRED_HEADERS) {
|
||||||
|
expect(headers[h], `missing header on 404: ${h}`).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue