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>
48 lines
1.8 KiB
TypeScript
48 lines
1.8 KiB
TypeScript
/**
|
||
* 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()
|
||
}
|
||
})
|
||
})
|