Skip to content

Commit d0f7b8c

Browse files
authored
Merge pull request #196 from wavyboy-build/fix/csp-security-headers
Add CSP and security response headers
2 parents d4232d0 + 7f11cfd commit d0f7b8c

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ Set all NEXT_PUBLIC_* vars in Vercel (or your hosting provider)
856856
857857
Enable HTTPS — Freighter requires a secure context (https://)
858858
859-
Add CSP headers allowing Stellar Horizon and Soroban RPC origins
859+
[x] Add CSP headers allowing Stellar Horizon and Soroban RPC origins — configured in next.config.mjs (headers()), connect-src derived from NEXT_PUBLIC_HORIZON_URL / NEXT_PUBLIC_SOROBAN_RPC_URL
860860
861861
Configure GitHub webhook secret and validate X-Hub-Signature-256 in the relayer
862862

__tests__/security-headers.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
const { getSecurityHeaders } = require('../security-headers');
5+
6+
describe('security headers', () => {
7+
const originalHorizon = process.env.NEXT_PUBLIC_HORIZON_URL;
8+
const originalSoroban = process.env.NEXT_PUBLIC_SOROBAN_RPC_URL;
9+
10+
afterEach(() => {
11+
process.env.NEXT_PUBLIC_HORIZON_URL = originalHorizon;
12+
process.env.NEXT_PUBLIC_SOROBAN_RPC_URL = originalSoroban;
13+
});
14+
15+
it('applies CSP and hardening headers to every route by default', () => {
16+
delete process.env.NEXT_PUBLIC_HORIZON_URL;
17+
delete process.env.NEXT_PUBLIC_SOROBAN_RPC_URL;
18+
19+
const byKey = Object.fromEntries(getSecurityHeaders().map((h) => [h.key, h.value]));
20+
21+
expect(byKey['Content-Security-Policy']).toContain("default-src 'self'");
22+
expect(byKey['Content-Security-Policy']).toContain('connect-src');
23+
expect(byKey['Content-Security-Policy']).toContain('https://horizon-testnet.stellar.org');
24+
expect(byKey['Content-Security-Policy']).toContain('https://soroban-testnet.stellar.org');
25+
expect(byKey['Content-Security-Policy']).toContain("frame-ancestors 'none'");
26+
expect(byKey['X-Content-Type-Options']).toBe('nosniff');
27+
expect(byKey['X-Frame-Options']).toBe('DENY');
28+
expect(byKey['Referrer-Policy']).toBe('strict-origin-when-cross-origin');
29+
});
30+
31+
it('derives connect-src from NEXT_PUBLIC_HORIZON_URL / NEXT_PUBLIC_SOROBAN_RPC_URL for self-hosted deployments', () => {
32+
process.env.NEXT_PUBLIC_HORIZON_URL = 'https://horizon.example.com';
33+
process.env.NEXT_PUBLIC_SOROBAN_RPC_URL = 'https://soroban.example.com';
34+
35+
const csp = getSecurityHeaders().find((h) => h.key === 'Content-Security-Policy').value;
36+
37+
expect(csp).toContain('https://horizon.example.com');
38+
expect(csp).toContain('https://soroban.example.com');
39+
});
40+
});

next.config.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
import { getSecurityHeaders } from './security-headers.js';
2+
13
/** @type {import('next').NextConfig} */
2-
const nextConfig = {};
4+
const nextConfig = {
5+
async headers() {
6+
return [
7+
{
8+
source: '/:path*',
9+
headers: getSecurityHeaders(),
10+
},
11+
];
12+
},
13+
};
314

415
export default nextConfig;

security-headers.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const DEFAULT_HORIZON_URL = 'https://horizon-testnet.stellar.org';
2+
const DEFAULT_SOROBAN_RPC_URL = 'https://soroban-testnet.stellar.org';
3+
4+
function getSecurityHeaders() {
5+
const connectSrcOrigins = Array.from(
6+
new Set(
7+
[
8+
"'self'",
9+
process.env.NEXT_PUBLIC_HORIZON_URL || DEFAULT_HORIZON_URL,
10+
process.env.NEXT_PUBLIC_SOROBAN_RPC_URL || DEFAULT_SOROBAN_RPC_URL,
11+
].filter(Boolean)
12+
)
13+
);
14+
15+
const contentSecurityPolicy = [
16+
"default-src 'self'",
17+
"script-src 'self' 'unsafe-inline'",
18+
"style-src 'self' 'unsafe-inline'",
19+
"img-src 'self' data:",
20+
`connect-src ${connectSrcOrigins.join(' ')}`,
21+
"frame-ancestors 'none'",
22+
].join('; ');
23+
24+
return [
25+
{ key: 'Content-Security-Policy', value: contentSecurityPolicy },
26+
{ key: 'X-Content-Type-Options', value: 'nosniff' },
27+
{ key: 'X-Frame-Options', value: 'DENY' },
28+
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
29+
];
30+
}
31+
32+
module.exports = { getSecurityHeaders };

0 commit comments

Comments
 (0)