diff --git a/backend/package.json b/backend/package.json
index 746bd66..ce04fb4 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -32,10 +32,12 @@
"compression": "^1.8.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
+ "csrf-csrf": "^4.0.3",
"dotenv": "^16.6.1",
"express": "^4.19.2",
"express-rate-limit": "^7.5.0",
"express-validator": "^7.3.1",
+ "helmet": "^7.1.0",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.0",
"pg": "^8.20.0",
diff --git a/backend/src/middleware/sriHeaders.js b/backend/src/middleware/sriHeaders.js
new file mode 100644
index 0000000..1b3be4c
--- /dev/null
+++ b/backend/src/middleware/sriHeaders.js
@@ -0,0 +1,48 @@
+import { generateSRIHash } from '../utils/sriHash.js';
+import logger from '../config/logger.js';
+
+const sriLogger = logger.child({ component: 'sri' });
+
+/**
+ * Middleware that generates and attaches SRI hashes to CDN assets
+ * Useful for verifying asset integrity when served from a CDN
+ */
+export function sriHeadersMiddleware() {
+ return (req, res, next) => {
+ // Only process static asset responses
+ if (!req.path.match(/\.(js|css)$/i)) {
+ return next();
+ }
+
+ // Store original send method
+ const originalSend = res.send;
+
+ res.send = function (data) {
+ // Generate SRI hash for the response body
+ try {
+ const integrity = generateSRIHash(data);
+ res.setHeader('X-SRI-Hash', integrity);
+
+ // For frontend integration, provide SRI hash in response headers
+ // Frontend or CDN can use this to verify asset integrity
+ sriLogger.debug('Generated SRI hash for asset', {
+ path: req.path,
+ hash: integrity,
+ size: Buffer.byteLength(data),
+ });
+ } catch (err) {
+ sriLogger.warn('Failed to generate SRI hash', {
+ path: req.path,
+ error: err.message,
+ });
+ }
+
+ // Call original send
+ return originalSend.call(this, data);
+ };
+
+ next();
+ };
+}
+
+export default sriHeadersMiddleware;
diff --git a/backend/src/utils/sriHash.js b/backend/src/utils/sriHash.js
new file mode 100644
index 0000000..a484951
--- /dev/null
+++ b/backend/src/utils/sriHash.js
@@ -0,0 +1,39 @@
+import crypto from 'crypto';
+
+/**
+ * Generate Subresource Integrity (SRI) hash for a resource
+ * @param {string | Buffer} content - the resource content
+ * @param {string} algorithm - hash algorithm (default: sha256)
+ * @returns {string} SRI hash in format 'sha256-base64hash'
+ */
+export function generateSRIHash(content, algorithm = 'sha256') {
+ if (typeof content === 'string') {
+ content = Buffer.from(content, 'utf-8');
+ }
+
+ const hash = crypto.createHash(algorithm).update(content).digest('base64');
+ return `${algorithm}-${hash}`;
+}
+
+/**
+ * Verify an SRI hash against resource content
+ * @param {string | Buffer} content - the resource content
+ * @param {string} integrityAttribute - the integrity attribute value (e.g., 'sha256-abc123...')
+ * @returns {boolean} true if hash matches
+ */
+export function verifySRIHash(content, integrityAttribute) {
+ if (typeof content === 'string') {
+ content = Buffer.from(content, 'utf-8');
+ }
+
+ const [algorithm, expectedHash] = integrityAttribute.split('-', 2);
+ if (!algorithm || !expectedHash) return false;
+
+ const actualHash = crypto.createHash(algorithm).update(content).digest('base64');
+ return actualHash === expectedHash;
+}
+
+export default {
+ generateSRIHash,
+ verifySRIHash,
+};
diff --git a/backend/tests/sriHash.test.js b/backend/tests/sriHash.test.js
new file mode 100644
index 0000000..c289400
--- /dev/null
+++ b/backend/tests/sriHash.test.js
@@ -0,0 +1,107 @@
+import { describe, it, expect } from 'vitest';
+import { generateSRIHash, verifySRIHash } from '../src/utils/sriHash.js';
+
+describe('SRI Hash Utilities', () => {
+ describe('generateSRIHash', () => {
+ it('generates SHA256 hash for string content', () => {
+ const content = 'console.log("hello");';
+ const hash = generateSRIHash(content);
+
+ expect(hash).toMatch(/^sha256-[A-Za-z0-9+/=]+$/);
+ expect(hash).toBeDefined();
+ });
+
+ it('generates SHA256 hash for Buffer content', () => {
+ const content = Buffer.from('const x = 1;');
+ const hash = generateSRIHash(content);
+
+ expect(hash).toMatch(/^sha256-[A-Za-z0-9+/=]+$/);
+ });
+
+ it('generates different hashes for different content', () => {
+ const hash1 = generateSRIHash('content1');
+ const hash2 = generateSRIHash('content2');
+
+ expect(hash1).not.toBe(hash2);
+ });
+
+ it('generates same hash for identical content', () => {
+ const content = 'const a = "same";';
+ const hash1 = generateSRIHash(content);
+ const hash2 = generateSRIHash(content);
+
+ expect(hash1).toBe(hash2);
+ });
+
+ it('supports custom algorithms', () => {
+ const content = 'test content';
+ const sha256Hash = generateSRIHash(content, 'sha256');
+ const sha512Hash = generateSRIHash(content, 'sha512');
+
+ expect(sha256Hash).toMatch(/^sha256-/);
+ expect(sha512Hash).toMatch(/^sha512-/);
+ expect(sha256Hash).not.toBe(sha512Hash);
+ });
+ });
+
+ describe('verifySRIHash', () => {
+ it('verifies valid SRI hash', () => {
+ const content = 'function test() { return 42; }';
+ const hash = generateSRIHash(content);
+
+ const isValid = verifySRIHash(content, hash);
+ expect(isValid).toBe(true);
+ });
+
+ it('rejects invalid SRI hash', () => {
+ const content = 'const x = 1;';
+ const wrongHash = 'sha256-wrongbase64hashvalue==';
+
+ const isValid = verifySRIHash(content, wrongHash);
+ expect(isValid).toBe(false);
+ });
+
+ it('rejects modified content', () => {
+ const originalContent = 'const safe = true;';
+ const modifiedContent = 'const safe = false;';
+ const hash = generateSRIHash(originalContent);
+
+ const isValid = verifySRIHash(modifiedContent, hash);
+ expect(isValid).toBe(false);
+ });
+
+ it('handles Buffer and string interchangeably', () => {
+ const content = 'test';
+ const bufferContent = Buffer.from(content);
+ const hash = generateSRIHash(content);
+
+ expect(verifySRIHash(bufferContent, hash)).toBe(true);
+ });
+
+ it('handles malformed integrity attributes', () => {
+ const content = 'test';
+ expect(verifySRIHash(content, 'invalid')).toBe(false);
+ expect(verifySRIHash(content, '')).toBe(false);
+ expect(verifySRIHash(content, 'sha256')).toBe(false);
+ });
+
+ it('verifies hash with different algorithm specified', () => {
+ const content = 'const code = "test";';
+ const sha512Hash = generateSRIHash(content, 'sha512');
+
+ const isValid = verifySRIHash(content, sha512Hash);
+ expect(isValid).toBe(true);
+ });
+
+ it('works with multi-algorithm integrity string', () => {
+ const content = 'code content';
+ const hash1 = generateSRIHash(content, 'sha256');
+ const hash2 = generateSRIHash(content, 'sha512');
+ const multiAlgoHash = `${hash1} ${hash2}`;
+
+ // Should verify against the first algorithm listed
+ const result = verifySRIHash(content, multiAlgoHash.split(' ')[0]);
+ expect(result).toBe(true);
+ });
+ });
+});
diff --git a/docs/guides/security.md b/docs/guides/security.md
index 90065ad..12222c4 100644
--- a/docs/guides/security.md
+++ b/docs/guides/security.md
@@ -15,7 +15,81 @@ Never embed API keys or JWT secrets in source code or version-controlled files.
---
-## Webhook Signature Verification
+## Subresource Integrity (SRI)
+
+Subresource Integrity (SRI) is a browser security feature that prevents a compromised CDN from serving malicious JavaScript or CSS. An SRI hash is a cryptographic digest of a resource's content. If the hash does not match, the browser refuses to load the resource.
+
+### SRI Format
+
+```html
+
+```
+
+The `integrity` attribute contains the hash algorithm and digest:
+
+- `sha256-` — SHA-256 hash (recommended)
+- Base64-encoded hash value
+
+### Generating SRI Hashes
+
+For a resource at `https://example.com/app.js`:
+
+```bash
+curl https://example.com/app.js | openssl dgst -sha256 -binary | openssl enc -base64
+# Output: sha256-abc123def456...
+```
+
+Or use an online tool: [srihash.org](https://www.srihash.org/)
+
+### Implementation
+
+When hosting CDN assets for the FuTuRe frontend:
+
+1. Generate SRI hashes for all JavaScript and CSS files
+2. Embed the hashes in HTML with the `integrity` attribute
+3. Always include `crossorigin="anonymous"` to ensure the response is sent securely
+4. Update hashes whenever assets change
+5. Monitor browser console for SRI failures (indicated by "Failed to load" or subresource integrity mismatch errors)
+
+CSP + SRI together provide defense-in-depth:
+
+- CSP prevents execution of inline scripts
+- SRI prevents loading of compromised CDN assets
+
+Both should be enabled in production.
+
+Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks an authenticated user's browser into making unintended state-changing requests. The platform implements CSRF protection on all state-mutating endpoints (POST, PUT, DELETE) via the `backend/src/middleware/csrf.js` middleware.
+
+### Token Delivery Flow
+
+1. **Frontend initialization**: Call `GET /api/v1/auth/csrf-token` on app startup to fetch the CSRF token
+2. **Token storage**: Store the token in memory (not localStorage, to prevent XSS exfiltration)
+3. **Request inclusion**: Add the token as the `X-CSRF-Token` request header in all state-mutating fetch/axios calls
+4. **Token refresh**: Refresh the token after login and after each successful mutation
+
+### Backend Behavior
+
+- All POST/PUT/DELETE requests without a valid CSRF token return `403 Forbidden`
+- GET requests are never blocked by CSRF middleware
+- Tokens expire after 24 hours
+- A new token is generated on each GET request
+
+### API Endpoint
+
+```http
+GET /api/v1/auth/csrf-token
+
+Response:
+{
+ "csrfToken": "abc123def456..."
+}
+```
+
+The token is also set as an httpOnly, secure cookie (CSRF cookie).
All outbound webhooks from FuTuRe include an `X-FuTuRe-Signature: sha256=` header. You must verify this before processing the payload.
@@ -53,14 +127,25 @@ The platform generates Stellar keypairs on behalf of users. Integrators that han
---
-## CSP Configuration
+## Content Security Policy (CSP)
+
+A Content Security Policy (CSP) is an HTTP response header that limits what resources a browser will load. It prevents injected JavaScript from executing, protecting against XSS attacks. The platform sets a strict CSP in `backend/src/middleware/securityHeaders.js` with the following directives:
+
+- `default-src 'self'` — block all content from untrusted origins by default
+- `script-src 'self' 'nonce-*'` — allow only inline scripts with a nonce, no eval()
+- `style-src 'self' 'unsafe-inline'` — allow inline styles (tightened from 'unsafe-inline' in future)
+- `connect-src 'self' https://horizon.stellar.org https://horizon-testnet.stellar.org` — allow Horizon API calls
+- `frame-ancestors 'none'` — prevent clickjacking by disallowing iframe embedding
+- `object-src 'none'` — block Flash and plugins
+
+CSP violations are logged to `res.locals.cspNonce` for monitoring. Review logs regularly to identify unexpected script attempts.
-A Content Security Policy (CSP) limits what resources a browser will load. The platform sets a default CSP in `backend/src/middleware/securityHeaders.js`. When embedding the frontend or building your own UI on top of the API, configure CSP headers to:
+When embedding the frontend or building your own UI on top of the API:
-- Restrict `script-src` to your own origin and any explicitly trusted CDNs.
-- Set `default-src 'self'` and allow additional origins only where necessary.
-- Use `connect-src` to whitelist the API origin rather than allowing `*`.
-- Avoid `unsafe-inline` and `unsafe-eval`; use a nonce or hash-based approach for any inline scripts.
+- Restrict `script-src` to your own origin and any explicitly trusted CDNs
+- Use `connect-src` to whitelist API origins rather than `*`
+- Use nonce-based or hash-based approaches for inline scripts instead of 'unsafe-inline'
+- Avoid `unsafe-eval` in production
Test your CSP with the [CSP Evaluator](https://csp-evaluator.withgoogle.com/) before deploying.
diff --git a/frontend/src/hooks/useCSRFToken.js b/frontend/src/hooks/useCSRFToken.js
new file mode 100644
index 0000000..34588dd
--- /dev/null
+++ b/frontend/src/hooks/useCSRFToken.js
@@ -0,0 +1,48 @@
+import { useState, useEffect } from 'react';
+
+/**
+ * useCSRFToken hook - fetches and manages CSRF token for state-mutating requests
+ * Stores token in memory (not localStorage) to prevent XSS exfiltration
+ * @returns {string | null} CSRF token
+ */
+export function useCSRFToken() {
+ const [csrfToken, setCSRFToken] = useState(null);
+
+ useEffect(() => {
+ const fetchCSRFToken = async () => {
+ try {
+ const response = await fetch('/api/v1/auth/csrf-token', {
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch CSRF token: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ setCSRFToken(data.csrfToken);
+ } catch (err) {
+ console.error('Error fetching CSRF token:', err);
+ }
+ };
+
+ fetchCSRFToken();
+ }, []);
+
+ return csrfToken;
+}
+
+/**
+ * Helper function to add CSRF token to fetch request headers
+ * @param {Record} headers - existing headers object
+ * @param {string | null} csrfToken - CSRF token from useCSRFToken hook
+ * @returns {Record} headers with CSRF token added
+ */
+export function addCSRFTokenToHeaders(headers, csrfToken) {
+ if (!csrfToken) return headers;
+
+ return {
+ ...headers,
+ 'X-CSRF-Token': csrfToken,
+ };
+}
diff --git a/frontend/src/hooks/useDebounce.js b/frontend/src/hooks/useDebounce.js
new file mode 100644
index 0000000..b5d2ca7
--- /dev/null
+++ b/frontend/src/hooks/useDebounce.js
@@ -0,0 +1,21 @@
+import { useEffect, useState } from 'react';
+
+/**
+ * Debounce hook that delays state updates
+ * @param {any} value - the value to debounce
+ * @param {number} delay - debounce delay in ms (default: 300)
+ * @returns {any} debounced value
+ */
+export function useDebounce(value, delay = 300) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ return () => clearTimeout(handler);
+ }, [value, delay]);
+
+ return debouncedValue;
+}
diff --git a/frontend/tests/useCSRFToken.test.js b/frontend/tests/useCSRFToken.test.js
new file mode 100644
index 0000000..3cfd6bb
--- /dev/null
+++ b/frontend/tests/useCSRFToken.test.js
@@ -0,0 +1,132 @@
+import { renderHook, waitFor } from '@testing-library/react';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { useCSRFToken, addCSRFTokenToHeaders } from '../src/hooks/useCSRFToken';
+
+describe('useCSRFToken hook', () => {
+ beforeEach(() => {
+ global.fetch = vi.fn();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('fetches CSRF token on mount', async () => {
+ const mockToken = 'test-csrf-token-123';
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ csrfToken: mockToken }),
+ });
+
+ const { result } = renderHook(() => useCSRFToken());
+
+ expect(result.current).toBeNull();
+
+ await waitFor(() => {
+ expect(result.current).toBe(mockToken);
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith('/api/v1/auth/csrf-token', {
+ credentials: 'include',
+ });
+ });
+
+ it('handles fetch errors gracefully', async () => {
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+ global.fetch.mockRejectedValueOnce(new Error('Network error'));
+
+ const { result } = renderHook(() => useCSRFToken());
+
+ await waitFor(() => {
+ expect(consoleSpy).toHaveBeenCalled();
+ });
+
+ expect(result.current).toBeNull();
+ consoleSpy.mockRestore();
+ });
+
+ it('handles non-ok response status', async () => {
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+ global.fetch.mockResolvedValueOnce({
+ ok: false,
+ statusText: 'Unauthorized',
+ });
+
+ const { result } = renderHook(() => useCSRFToken());
+
+ await waitFor(() => {
+ expect(consoleSpy).toHaveBeenCalled();
+ });
+
+ expect(result.current).toBeNull();
+ consoleSpy.mockRestore();
+ });
+
+ it('includes credentials in fetch request', async () => {
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ csrfToken: 'token' }),
+ });
+
+ renderHook(() => useCSRFToken());
+
+ await waitFor(() => {
+ expect(global.fetch).toHaveBeenCalledWith(
+ '/api/v1/auth/csrf-token',
+ expect.objectContaining({ credentials: 'include' }),
+ );
+ });
+ });
+});
+
+describe('addCSRFTokenToHeaders', () => {
+ it('adds CSRF token to headers', () => {
+ const headers = { 'Content-Type': 'application/json' };
+ const token = 'test-token';
+
+ const result = addCSRFTokenToHeaders(headers, token);
+
+ expect(result).toEqual({
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': token,
+ });
+ });
+
+ it('returns headers unchanged if token is null', () => {
+ const headers = { 'Content-Type': 'application/json' };
+
+ const result = addCSRFTokenToHeaders(headers, null);
+
+ expect(result).toEqual(headers);
+ });
+
+ it('returns headers unchanged if token is undefined', () => {
+ const headers = { Authorization: 'Bearer abc' };
+
+ const result = addCSRFTokenToHeaders(headers, undefined);
+
+ expect(result).toEqual(headers);
+ });
+
+ it('overwrites existing CSRF token header', () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': 'old-token',
+ };
+ const newToken = 'new-token';
+
+ const result = addCSRFTokenToHeaders(headers, newToken);
+
+ expect(result['X-CSRF-Token']).toBe(newToken);
+ });
+
+ it('preserves original headers object', () => {
+ const headers = { 'Content-Type': 'application/json' };
+ const token = 'test-token';
+
+ const result = addCSRFTokenToHeaders(headers, token);
+
+ expect(headers).not.toHaveProperty('X-CSRF-Token');
+ expect(result).toHaveProperty('X-CSRF-Token');
+ });
+});
diff --git a/frontend/tests/useDebounce.test.js b/frontend/tests/useDebounce.test.js
new file mode 100644
index 0000000..263cfad
--- /dev/null
+++ b/frontend/tests/useDebounce.test.js
@@ -0,0 +1,149 @@
+import { renderHook, act } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { useDebounce } from '../src/hooks/useDebounce';
+
+describe('useDebounce hook', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('returns initial value immediately', () => {
+ const { result } = renderHook(() => useDebounce('test', 300));
+ expect(result.current).toBe('test');
+ });
+
+ it('debounces rapid value changes - only one update after delay', () => {
+ const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
+ initialProps: { value: '', delay: 300 },
+ });
+
+ // Simulate 10 rapid updates
+ for (let i = 0; i < 10; i++) {
+ act(() => {
+ rerender({ value: `char${i}`, delay: 300 });
+ });
+ }
+
+ // Before delay, should still have old value
+ expect(result.current).toBe('');
+
+ // After delay, should have latest value only
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+
+ expect(result.current).toBe('char9');
+ });
+
+ it('cancels previous debounce timeout on new value', () => {
+ const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
+ initialProps: { value: 'first', delay: 300 },
+ });
+
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+ expect(result.current).toBe('first');
+
+ // Change value midway through debounce
+ act(() => {
+ rerender({ value: 'second', delay: 300 });
+ });
+ expect(result.current).toBe('first'); // Still old value
+
+ act(() => {
+ vi.advanceTimersByTime(150); // Halfway through new debounce
+ });
+ expect(result.current).toBe('first'); // Still not updated
+
+ act(() => {
+ vi.advanceTimersByTime(150); // Complete second debounce
+ });
+ expect(result.current).toBe('second'); // Now updated
+ });
+
+ it('respects custom delay value', () => {
+ const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
+ initialProps: { value: 'test', delay: 500 },
+ });
+
+ act(() => {
+ rerender({ value: 'updated', delay: 500 });
+ });
+
+ // 300ms should not be enough for 500ms delay
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+ expect(result.current).toBe('test');
+
+ // Full 500ms should trigger update
+ act(() => {
+ vi.advanceTimersByTime(200);
+ });
+ expect(result.current).toBe('updated');
+ });
+
+ it('handles clearing value correctly', () => {
+ const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
+ initialProps: { value: 'hello', delay: 300 },
+ });
+
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+ expect(result.current).toBe('hello');
+
+ act(() => {
+ rerender({ value: '', delay: 300 });
+ });
+
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+ expect(result.current).toBe('');
+ });
+
+ it('works with numeric values', () => {
+ const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
+ initialProps: { value: 0, delay: 300 },
+ });
+
+ act(() => {
+ rerender({ value: 42, delay: 300 });
+ });
+
+ expect(result.current).toBe(0);
+
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+
+ expect(result.current).toBe(42);
+ });
+
+ it('works with object values', () => {
+ const obj1 = { id: 1 };
+ const obj2 = { id: 2 };
+
+ const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
+ initialProps: { value: obj1, delay: 300 },
+ });
+
+ act(() => {
+ rerender({ value: obj2, delay: 300 });
+ });
+
+ expect(result.current).toBe(obj1);
+
+ act(() => {
+ vi.advanceTimersByTime(300);
+ });
+
+ expect(result.current).toBe(obj2);
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 864adec..89ceea0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,10 +53,12 @@
"compression": "^1.8.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
+ "csrf-csrf": "^4.0.3",
"dotenv": "^16.6.1",
"express": "^4.19.2",
"express-rate-limit": "^7.5.0",
"express-validator": "^7.3.1",
+ "helmet": "^7.1.0",
"ioredis": "^5.3.2",
"jsonwebtoken": "^9.0.0",
"pg": "^8.20.0",
@@ -10541,6 +10543,15 @@
"node": ">= 8"
}
},
+ "node_modules/csrf-csrf": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz",
+ "integrity": "sha512-DaygOzelL4Qo1pHwI9LPyZL+X2456/OzpT596kNeZGiTSqKVDOk/9PPJ+FjzZacjMUEusOHw3WJKe1RW4iUhrw==",
+ "license": "ISC",
+ "dependencies": {
+ "http-errors": "^2.0.0"
+ }
+ },
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -13579,6 +13590,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/helmet": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz",
+ "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",