@nextsparkjs/theme-default 0.1.0-beta.49 → 0.1.0-beta.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/ai/chat/stream/route.ts +4 -1
- package/api/ai/orchestrator/route.ts +10 -3
- package/api/ai/single-agent/route.ts +10 -3
- package/api/ai/usage/route.ts +4 -1
- package/blocks/text-content/component.tsx +10 -8
- package/config/dashboard.config.ts +14 -0
- package/config/permissions.config.ts +11 -0
- package/messages/en/admin.json +12 -1
- package/messages/es/admin.json +12 -1
- package/migrations/093_pages_sample_data.sql +7 -7
- package/migrations/098_patterns_sample_data.sql +234 -0
- package/package.json +3 -3
- package/tests/cypress/e2e/_utils/selectors/block-editor.bdd.md +127 -3
- package/tests/cypress/e2e/_utils/selectors/block-editor.cy.ts +124 -0
- package/tests/cypress/e2e/ai/chat-api.cy.ts +50 -38
- package/tests/cypress/e2e/api/_core/security/security-headers.cy.ts +601 -0
- package/tests/cypress/e2e/patterns/patterns-in-pages.cy.ts +367 -0
- package/tests/cypress/fixtures/entities.json +9 -0
- package/tests/cypress/src/entities/PatternsPOM.ts +329 -0
- package/tests/cypress/src/entities/index.ts +2 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Security Headers Tests
|
|
5
|
+
*
|
|
6
|
+
* Tests for HTTP security headers on all routes.
|
|
7
|
+
* Verifies CSP, X-Frame-Options, X-Content-Type-Options, and other security headers.
|
|
8
|
+
*
|
|
9
|
+
* @see SEC-004 Security Headers Implementation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as allure from 'allure-cypress'
|
|
13
|
+
|
|
14
|
+
describe('Security Headers', {
|
|
15
|
+
tags: ['@api', '@security', '@headers', '@regression']
|
|
16
|
+
}, () => {
|
|
17
|
+
|
|
18
|
+
const BASE_URL = Cypress.config('baseUrl') || 'http://localhost:5173'
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
allure.epic('Security')
|
|
22
|
+
allure.feature('HTTP Headers')
|
|
23
|
+
allure.story('Security Headers')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// Core Security Headers Tests
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
describe('Core Security Headers on HTML Pages', () => {
|
|
31
|
+
|
|
32
|
+
it('SEC_HDR_001: Homepage should have X-Content-Type-Options header', { tags: '@smoke' }, () => {
|
|
33
|
+
allure.severity('critical')
|
|
34
|
+
cy.request({
|
|
35
|
+
method: 'GET',
|
|
36
|
+
url: `${BASE_URL}/`,
|
|
37
|
+
failOnStatusCode: false
|
|
38
|
+
}).then((response) => {
|
|
39
|
+
expect(response.headers).to.have.property('x-content-type-options', 'nosniff')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('SEC_HDR_002: Homepage should have X-Frame-Options header', { tags: '@smoke' }, () => {
|
|
44
|
+
allure.severity('critical')
|
|
45
|
+
cy.request({
|
|
46
|
+
method: 'GET',
|
|
47
|
+
url: `${BASE_URL}/`,
|
|
48
|
+
failOnStatusCode: false
|
|
49
|
+
}).then((response) => {
|
|
50
|
+
expect(response.headers).to.have.property('x-frame-options', 'DENY')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('SEC_HDR_003: Homepage should have X-XSS-Protection header', () => {
|
|
55
|
+
allure.severity('normal')
|
|
56
|
+
cy.request({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
url: `${BASE_URL}/`,
|
|
59
|
+
failOnStatusCode: false
|
|
60
|
+
}).then((response) => {
|
|
61
|
+
expect(response.headers).to.have.property('x-xss-protection', '1; mode=block')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('SEC_HDR_004: Homepage should have Referrer-Policy header', () => {
|
|
66
|
+
allure.severity('normal')
|
|
67
|
+
cy.request({
|
|
68
|
+
method: 'GET',
|
|
69
|
+
url: `${BASE_URL}/`,
|
|
70
|
+
failOnStatusCode: false
|
|
71
|
+
}).then((response) => {
|
|
72
|
+
expect(response.headers).to.have.property('referrer-policy', 'strict-origin-when-cross-origin')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('SEC_HDR_005: Homepage should have Permissions-Policy header', () => {
|
|
77
|
+
allure.severity('normal')
|
|
78
|
+
cy.request({
|
|
79
|
+
method: 'GET',
|
|
80
|
+
url: `${BASE_URL}/`,
|
|
81
|
+
failOnStatusCode: false
|
|
82
|
+
}).then((response) => {
|
|
83
|
+
expect(response.headers).to.have.property('permissions-policy')
|
|
84
|
+
expect(response.headers['permissions-policy']).to.include('camera=()')
|
|
85
|
+
expect(response.headers['permissions-policy']).to.include('microphone=()')
|
|
86
|
+
expect(response.headers['permissions-policy']).to.include('geolocation=()')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('SEC_HDR_006: Homepage should have Content-Security-Policy header', { tags: '@smoke' }, () => {
|
|
91
|
+
allure.severity('critical')
|
|
92
|
+
cy.request({
|
|
93
|
+
method: 'GET',
|
|
94
|
+
url: `${BASE_URL}/`,
|
|
95
|
+
failOnStatusCode: false
|
|
96
|
+
}).then((response) => {
|
|
97
|
+
expect(response.headers).to.have.property('content-security-policy')
|
|
98
|
+
const csp = response.headers['content-security-policy']
|
|
99
|
+
|
|
100
|
+
// Validate key CSP directives
|
|
101
|
+
expect(csp).to.include("default-src 'self'")
|
|
102
|
+
expect(csp).to.include("script-src")
|
|
103
|
+
expect(csp).to.include("style-src")
|
|
104
|
+
expect(csp).to.include("img-src")
|
|
105
|
+
expect(csp).to.include("frame-ancestors 'none'")
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// ============================================
|
|
111
|
+
// API Endpoints Security Headers
|
|
112
|
+
// ============================================
|
|
113
|
+
|
|
114
|
+
describe('Security Headers on API Endpoints', () => {
|
|
115
|
+
|
|
116
|
+
it('SEC_HDR_010: API endpoint should have X-Content-Type-Options', () => {
|
|
117
|
+
allure.severity('critical')
|
|
118
|
+
cy.request({
|
|
119
|
+
method: 'GET',
|
|
120
|
+
url: `${BASE_URL}/api/v1/theme`,
|
|
121
|
+
failOnStatusCode: false
|
|
122
|
+
}).then((response) => {
|
|
123
|
+
expect(response.headers).to.have.property('x-content-type-options', 'nosniff')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('SEC_HDR_011: API endpoint should have X-Frame-Options', () => {
|
|
128
|
+
allure.severity('critical')
|
|
129
|
+
cy.request({
|
|
130
|
+
method: 'GET',
|
|
131
|
+
url: `${BASE_URL}/api/v1/theme`,
|
|
132
|
+
failOnStatusCode: false
|
|
133
|
+
}).then((response) => {
|
|
134
|
+
expect(response.headers).to.have.property('x-frame-options', 'DENY')
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('SEC_HDR_012: API endpoint should have Content-Security-Policy', () => {
|
|
139
|
+
allure.severity('normal')
|
|
140
|
+
cy.request({
|
|
141
|
+
method: 'GET',
|
|
142
|
+
url: `${BASE_URL}/api/v1/theme`,
|
|
143
|
+
failOnStatusCode: false
|
|
144
|
+
}).then((response) => {
|
|
145
|
+
expect(response.headers).to.have.property('content-security-policy')
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// ============================================
|
|
151
|
+
// Login Page Security Headers
|
|
152
|
+
// ============================================
|
|
153
|
+
|
|
154
|
+
describe('Security Headers on Auth Pages', () => {
|
|
155
|
+
|
|
156
|
+
it('SEC_HDR_020: Login page should have all security headers', { tags: '@smoke' }, () => {
|
|
157
|
+
allure.severity('critical')
|
|
158
|
+
cy.request({
|
|
159
|
+
method: 'GET',
|
|
160
|
+
url: `${BASE_URL}/login`,
|
|
161
|
+
failOnStatusCode: false
|
|
162
|
+
}).then((response) => {
|
|
163
|
+
// All critical security headers
|
|
164
|
+
expect(response.headers).to.have.property('x-content-type-options', 'nosniff')
|
|
165
|
+
expect(response.headers).to.have.property('x-frame-options', 'DENY')
|
|
166
|
+
expect(response.headers).to.have.property('x-xss-protection', '1; mode=block')
|
|
167
|
+
expect(response.headers).to.have.property('referrer-policy', 'strict-origin-when-cross-origin')
|
|
168
|
+
expect(response.headers).to.have.property('content-security-policy')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('SEC_HDR_021: Register page should have all security headers', () => {
|
|
173
|
+
allure.severity('normal')
|
|
174
|
+
cy.request({
|
|
175
|
+
method: 'GET',
|
|
176
|
+
url: `${BASE_URL}/register`,
|
|
177
|
+
failOnStatusCode: false
|
|
178
|
+
}).then((response) => {
|
|
179
|
+
expect(response.headers).to.have.property('x-content-type-options', 'nosniff')
|
|
180
|
+
expect(response.headers).to.have.property('x-frame-options', 'DENY')
|
|
181
|
+
expect(response.headers).to.have.property('content-security-policy')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// CSP Directive Validation
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
describe('CSP Directive Validation', () => {
|
|
191
|
+
|
|
192
|
+
it('SEC_HDR_030: CSP should allow Stripe scripts', () => {
|
|
193
|
+
allure.severity('normal')
|
|
194
|
+
cy.request({
|
|
195
|
+
method: 'GET',
|
|
196
|
+
url: `${BASE_URL}/`,
|
|
197
|
+
failOnStatusCode: false
|
|
198
|
+
}).then((response) => {
|
|
199
|
+
const csp = response.headers['content-security-policy']
|
|
200
|
+
expect(csp).to.include('https://js.stripe.com')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('SEC_HDR_031: CSP should allow Stripe API connections', () => {
|
|
205
|
+
allure.severity('normal')
|
|
206
|
+
cy.request({
|
|
207
|
+
method: 'GET',
|
|
208
|
+
url: `${BASE_URL}/`,
|
|
209
|
+
failOnStatusCode: false
|
|
210
|
+
}).then((response) => {
|
|
211
|
+
const csp = response.headers['content-security-policy']
|
|
212
|
+
expect(csp).to.include('https://api.stripe.com')
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('SEC_HDR_032: CSP should allow Stripe iframes', () => {
|
|
217
|
+
allure.severity('normal')
|
|
218
|
+
cy.request({
|
|
219
|
+
method: 'GET',
|
|
220
|
+
url: `${BASE_URL}/`,
|
|
221
|
+
failOnStatusCode: false
|
|
222
|
+
}).then((response) => {
|
|
223
|
+
const csp = response.headers['content-security-policy']
|
|
224
|
+
expect(csp).to.include('frame-src')
|
|
225
|
+
expect(csp).to.include('https://js.stripe.com')
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('SEC_HDR_033: CSP should allow data: URIs for images', () => {
|
|
230
|
+
allure.severity('normal')
|
|
231
|
+
cy.request({
|
|
232
|
+
method: 'GET',
|
|
233
|
+
url: `${BASE_URL}/`,
|
|
234
|
+
failOnStatusCode: false
|
|
235
|
+
}).then((response) => {
|
|
236
|
+
const csp = response.headers['content-security-policy']
|
|
237
|
+
expect(csp).to.include('img-src')
|
|
238
|
+
expect(csp).to.include('data:')
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('SEC_HDR_034: CSP should block framing by other sites', () => {
|
|
243
|
+
allure.severity('critical')
|
|
244
|
+
cy.request({
|
|
245
|
+
method: 'GET',
|
|
246
|
+
url: `${BASE_URL}/`,
|
|
247
|
+
failOnStatusCode: false
|
|
248
|
+
}).then((response) => {
|
|
249
|
+
const csp = response.headers['content-security-policy']
|
|
250
|
+
expect(csp).to.include("frame-ancestors 'none'")
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('SEC_HDR_035: CSP should block object/plugin loading', () => {
|
|
255
|
+
allure.severity('normal')
|
|
256
|
+
cy.request({
|
|
257
|
+
method: 'GET',
|
|
258
|
+
url: `${BASE_URL}/`,
|
|
259
|
+
failOnStatusCode: false
|
|
260
|
+
}).then((response) => {
|
|
261
|
+
const csp = response.headers['content-security-policy']
|
|
262
|
+
expect(csp).to.include("object-src 'none'")
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('SEC_HDR_036: CSP should restrict base-uri to self', () => {
|
|
267
|
+
allure.severity('normal')
|
|
268
|
+
cy.request({
|
|
269
|
+
method: 'GET',
|
|
270
|
+
url: `${BASE_URL}/`,
|
|
271
|
+
failOnStatusCode: false
|
|
272
|
+
}).then((response) => {
|
|
273
|
+
const csp = response.headers['content-security-policy']
|
|
274
|
+
expect(csp).to.include("base-uri 'self'")
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('SEC_HDR_037: CSP img-src should allow specific trusted domains', () => {
|
|
279
|
+
allure.severity('normal')
|
|
280
|
+
cy.request({
|
|
281
|
+
method: 'GET',
|
|
282
|
+
url: `${BASE_URL}/`,
|
|
283
|
+
failOnStatusCode: false
|
|
284
|
+
}).then((response) => {
|
|
285
|
+
const csp = response.headers['content-security-policy']
|
|
286
|
+
// Should include specific domains, not https: wildcard
|
|
287
|
+
expect(csp).to.include('img-src')
|
|
288
|
+
expect(csp).to.include('lh3.googleusercontent.com')
|
|
289
|
+
expect(csp).to.include('images.unsplash.com')
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// ============================================
|
|
295
|
+
// CORS Headers (API only)
|
|
296
|
+
// ============================================
|
|
297
|
+
|
|
298
|
+
describe('CORS Headers on API', () => {
|
|
299
|
+
|
|
300
|
+
it('SEC_HDR_040: API should have CORS headers', () => {
|
|
301
|
+
allure.severity('normal')
|
|
302
|
+
cy.request({
|
|
303
|
+
method: 'GET',
|
|
304
|
+
url: `${BASE_URL}/api/v1/theme`,
|
|
305
|
+
failOnStatusCode: false
|
|
306
|
+
}).then((response) => {
|
|
307
|
+
expect(response.headers).to.have.property('access-control-allow-origin')
|
|
308
|
+
expect(response.headers).to.have.property('access-control-allow-methods')
|
|
309
|
+
expect(response.headers).to.have.property('access-control-allow-credentials', 'true')
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// ============================================
|
|
315
|
+
// CSP Violation Reporting
|
|
316
|
+
// ============================================
|
|
317
|
+
|
|
318
|
+
describe('CSP Violation Reporting', () => {
|
|
319
|
+
|
|
320
|
+
it('SEC_HDR_050: CSP should include report-uri and report-to directives', () => {
|
|
321
|
+
allure.severity('normal')
|
|
322
|
+
cy.request({
|
|
323
|
+
method: 'GET',
|
|
324
|
+
url: `${BASE_URL}/`,
|
|
325
|
+
failOnStatusCode: false
|
|
326
|
+
}).then((response) => {
|
|
327
|
+
const csp = response.headers['content-security-policy']
|
|
328
|
+
// Legacy report-uri for older browsers
|
|
329
|
+
expect(csp).to.include('report-uri /api/csp-report')
|
|
330
|
+
// Modern report-to for newer browsers
|
|
331
|
+
expect(csp).to.include('report-to csp-endpoint')
|
|
332
|
+
// Reporting-Endpoints header should also be present
|
|
333
|
+
expect(response.headers).to.have.property('reporting-endpoints')
|
|
334
|
+
expect(response.headers['reporting-endpoints']).to.include('csp-endpoint')
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('SEC_HDR_051: CSP report endpoint should accept violation reports', () => {
|
|
339
|
+
allure.severity('normal')
|
|
340
|
+
const mockViolation = {
|
|
341
|
+
'csp-report': {
|
|
342
|
+
'document-uri': 'https://example.com/page',
|
|
343
|
+
'referrer': '',
|
|
344
|
+
'violated-directive': 'script-src',
|
|
345
|
+
'effective-directive': 'script-src',
|
|
346
|
+
'original-policy': "default-src 'self'",
|
|
347
|
+
'blocked-uri': 'https://evil.com/malicious.js',
|
|
348
|
+
'status-code': 200
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
cy.request({
|
|
353
|
+
method: 'POST',
|
|
354
|
+
url: `${BASE_URL}/api/csp-report`,
|
|
355
|
+
headers: {
|
|
356
|
+
'Content-Type': 'application/csp-report'
|
|
357
|
+
},
|
|
358
|
+
body: mockViolation,
|
|
359
|
+
failOnStatusCode: false
|
|
360
|
+
}).then((response) => {
|
|
361
|
+
// CSP report endpoint should return 204 No Content
|
|
362
|
+
expect(response.status).to.eq(204)
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('SEC_HDR_052: CSP report endpoint should handle invalid content type', () => {
|
|
367
|
+
allure.severity('minor')
|
|
368
|
+
cy.request({
|
|
369
|
+
method: 'POST',
|
|
370
|
+
url: `${BASE_URL}/api/csp-report`,
|
|
371
|
+
headers: {
|
|
372
|
+
'Content-Type': 'text/plain'
|
|
373
|
+
},
|
|
374
|
+
body: 'invalid',
|
|
375
|
+
failOnStatusCode: false
|
|
376
|
+
}).then((response) => {
|
|
377
|
+
expect(response.status).to.eq(400)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// ============================================
|
|
383
|
+
// Negative Security Tests
|
|
384
|
+
// ============================================
|
|
385
|
+
|
|
386
|
+
describe('Negative Security Tests', () => {
|
|
387
|
+
|
|
388
|
+
it('SEC_HDR_060: CSP should NOT allow unsafe-eval in production mode', () => {
|
|
389
|
+
allure.severity('critical')
|
|
390
|
+
cy.request({
|
|
391
|
+
method: 'GET',
|
|
392
|
+
url: `${BASE_URL}/`,
|
|
393
|
+
failOnStatusCode: false
|
|
394
|
+
}).then((response) => {
|
|
395
|
+
const csp = response.headers['content-security-policy']
|
|
396
|
+
// In development, unsafe-eval is allowed for Next.js hot reload
|
|
397
|
+
// This test documents the expected behavior - in production, it would be blocked
|
|
398
|
+
// For now, we verify the CSP is present and properly formatted
|
|
399
|
+
expect(csp).to.be.a('string')
|
|
400
|
+
expect(csp.length).to.be.greaterThan(50)
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('SEC_HDR_061: CSP should NOT use https: wildcard for images', () => {
|
|
405
|
+
allure.severity('critical')
|
|
406
|
+
cy.request({
|
|
407
|
+
method: 'GET',
|
|
408
|
+
url: `${BASE_URL}/`,
|
|
409
|
+
failOnStatusCode: false
|
|
410
|
+
}).then((response) => {
|
|
411
|
+
const csp = response.headers['content-security-policy']
|
|
412
|
+
// Extract img-src directive
|
|
413
|
+
const imgSrcMatch = csp.match(/img-src[^;]+/)
|
|
414
|
+
if (imgSrcMatch) {
|
|
415
|
+
const imgSrc = imgSrcMatch[0]
|
|
416
|
+
// Should NOT have bare 'https:' which allows any HTTPS domain
|
|
417
|
+
// Should have specific domains instead
|
|
418
|
+
expect(imgSrc).to.not.match(/\shttps:\s/)
|
|
419
|
+
expect(imgSrc).to.not.match(/\shttps:;/)
|
|
420
|
+
expect(imgSrc).to.not.match(/\shttps:$/)
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('SEC_HDR_062: CSP should NOT use wss: wildcard in production', () => {
|
|
426
|
+
allure.severity('normal')
|
|
427
|
+
cy.request({
|
|
428
|
+
method: 'GET',
|
|
429
|
+
url: `${BASE_URL}/`,
|
|
430
|
+
failOnStatusCode: false
|
|
431
|
+
}).then((response) => {
|
|
432
|
+
const csp = response.headers['content-security-policy']
|
|
433
|
+
// In development, wss: is allowed for hot reload
|
|
434
|
+
// This test documents the expected behavior
|
|
435
|
+
expect(csp).to.include('connect-src')
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('SEC_HDR_063: X-Frame-Options should deny all framing', () => {
|
|
440
|
+
allure.severity('critical')
|
|
441
|
+
cy.request({
|
|
442
|
+
method: 'GET',
|
|
443
|
+
url: `${BASE_URL}/`,
|
|
444
|
+
failOnStatusCode: false
|
|
445
|
+
}).then((response) => {
|
|
446
|
+
// Should be DENY, not SAMEORIGIN or ALLOW-FROM
|
|
447
|
+
expect(response.headers['x-frame-options']).to.eq('DENY')
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('SEC_HDR_064: Permissions-Policy should disable dangerous features', () => {
|
|
452
|
+
allure.severity('normal')
|
|
453
|
+
cy.request({
|
|
454
|
+
method: 'GET',
|
|
455
|
+
url: `${BASE_URL}/`,
|
|
456
|
+
failOnStatusCode: false
|
|
457
|
+
}).then((response) => {
|
|
458
|
+
const policy = response.headers['permissions-policy']
|
|
459
|
+
// These features should be completely disabled (empty allowlist)
|
|
460
|
+
expect(policy).to.include('camera=()')
|
|
461
|
+
expect(policy).to.include('microphone=()')
|
|
462
|
+
expect(policy).to.include('geolocation=()')
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
// ============================================
|
|
468
|
+
// HSTS Configuration (Production Only)
|
|
469
|
+
// ============================================
|
|
470
|
+
|
|
471
|
+
describe('HSTS Configuration', () => {
|
|
472
|
+
|
|
473
|
+
it('SEC_HDR_070: Should have correct HSTS behavior based on environment', () => {
|
|
474
|
+
allure.severity('normal')
|
|
475
|
+
// HSTS is only enabled in production
|
|
476
|
+
// In production: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
|
477
|
+
// In development: Header should NOT be present
|
|
478
|
+
|
|
479
|
+
cy.request({
|
|
480
|
+
method: 'GET',
|
|
481
|
+
url: `${BASE_URL}/`,
|
|
482
|
+
failOnStatusCode: false
|
|
483
|
+
}).then((response) => {
|
|
484
|
+
const isProduction = Cypress.env('NODE_ENV') === 'production'
|
|
485
|
+
|
|
486
|
+
if (isProduction) {
|
|
487
|
+
// Production: HSTS must be present with correct values
|
|
488
|
+
expect(response.headers).to.have.property('strict-transport-security')
|
|
489
|
+
const hsts = response.headers['strict-transport-security']
|
|
490
|
+
expect(hsts).to.include('max-age=31536000')
|
|
491
|
+
expect(hsts).to.include('includeSubDomains')
|
|
492
|
+
expect(hsts).to.include('preload')
|
|
493
|
+
} else {
|
|
494
|
+
// Development: HSTS should NOT be set (to avoid localhost issues)
|
|
495
|
+
expect(response.headers).to.not.have.property('strict-transport-security')
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('SEC_HDR_071: Development should NOT have HSTS header', () => {
|
|
501
|
+
allure.severity('normal')
|
|
502
|
+
// This test specifically verifies dev behavior
|
|
503
|
+
// HSTS on localhost would cause browser issues
|
|
504
|
+
|
|
505
|
+
cy.request({
|
|
506
|
+
method: 'GET',
|
|
507
|
+
url: `${BASE_URL}/`,
|
|
508
|
+
failOnStatusCode: false
|
|
509
|
+
}).then((response) => {
|
|
510
|
+
const isProduction = Cypress.env('NODE_ENV') === 'production'
|
|
511
|
+
|
|
512
|
+
if (!isProduction) {
|
|
513
|
+
// Explicitly verify no HSTS in development
|
|
514
|
+
expect(response.headers).to.not.have.property('strict-transport-security')
|
|
515
|
+
} else {
|
|
516
|
+
// Skip in production - SEC_HDR_070 covers this
|
|
517
|
+
cy.log('Skipping - running in production mode')
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// ============================================
|
|
524
|
+
// Rate Limiting Tests
|
|
525
|
+
// ============================================
|
|
526
|
+
|
|
527
|
+
describe('CSP Report Rate Limiting', () => {
|
|
528
|
+
|
|
529
|
+
it('SEC_HDR_080: CSP report endpoint should work and optionally include rate limit headers', () => {
|
|
530
|
+
allure.severity('normal')
|
|
531
|
+
const mockViolation = {
|
|
532
|
+
'csp-report': {
|
|
533
|
+
'document-uri': 'https://example.com/page',
|
|
534
|
+
'violated-directive': 'script-src',
|
|
535
|
+
'blocked-uri': 'https://evil.com/malicious.js',
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
cy.request({
|
|
540
|
+
method: 'POST',
|
|
541
|
+
url: `${BASE_URL}/api/csp-report`,
|
|
542
|
+
headers: {
|
|
543
|
+
'Content-Type': 'application/csp-report'
|
|
544
|
+
},
|
|
545
|
+
body: mockViolation,
|
|
546
|
+
failOnStatusCode: false
|
|
547
|
+
}).then((response) => {
|
|
548
|
+
// CSP report should succeed with 204
|
|
549
|
+
expect(response.status).to.eq(204)
|
|
550
|
+
|
|
551
|
+
// Rate limit headers are optional (depend on @nextsparkjs/core/lib/api availability)
|
|
552
|
+
// If present, verify they have valid values
|
|
553
|
+
if (response.headers['x-ratelimit-limit']) {
|
|
554
|
+
expect(response.headers).to.have.property('x-ratelimit-remaining')
|
|
555
|
+
expect(response.headers).to.have.property('x-ratelimit-reset')
|
|
556
|
+
cy.log('Rate limiting is enabled')
|
|
557
|
+
} else {
|
|
558
|
+
cy.log('Rate limiting not available - skipping rate limit header checks')
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// ============================================
|
|
565
|
+
// CORS x-api-key Header Tests
|
|
566
|
+
// ============================================
|
|
567
|
+
|
|
568
|
+
describe('CORS API Key Header', () => {
|
|
569
|
+
|
|
570
|
+
it('SEC_HDR_090: API CORS should allow x-api-key header', () => {
|
|
571
|
+
allure.severity('critical')
|
|
572
|
+
cy.request({
|
|
573
|
+
method: 'GET',
|
|
574
|
+
url: `${BASE_URL}/api/v1/theme`,
|
|
575
|
+
failOnStatusCode: false
|
|
576
|
+
}).then((response) => {
|
|
577
|
+
expect(response.headers).to.have.property('access-control-allow-headers')
|
|
578
|
+
const allowedHeaders = response.headers['access-control-allow-headers'].toLowerCase()
|
|
579
|
+
expect(allowedHeaders).to.include('x-api-key')
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('SEC_HDR_091: API CORS headers should include all required headers', () => {
|
|
584
|
+
allure.severity('normal')
|
|
585
|
+
cy.request({
|
|
586
|
+
method: 'GET',
|
|
587
|
+
url: `${BASE_URL}/api/v1/theme`,
|
|
588
|
+
failOnStatusCode: false
|
|
589
|
+
}).then((response) => {
|
|
590
|
+
expect(response.headers).to.have.property('access-control-allow-headers')
|
|
591
|
+
const allowedHeaders = response.headers['access-control-allow-headers'].toLowerCase()
|
|
592
|
+
|
|
593
|
+
// Verify all critical headers are allowed
|
|
594
|
+
expect(allowedHeaders).to.include('content-type')
|
|
595
|
+
expect(allowedHeaders).to.include('authorization')
|
|
596
|
+
expect(allowedHeaders).to.include('x-api-key')
|
|
597
|
+
expect(allowedHeaders).to.include('cookie')
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
})
|