Skip to content

Commit 0e74efb

Browse files
Agastya18arielvino
andauthored
chore: add ngnix security headers (#964)
Co-authored-by: Ariel Vinograd <71963953+arielvino@users.noreply.github.com>
1 parent 973b330 commit 0e74efb

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

.github/workflows/validate.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,20 @@ jobs:
294294
- name: Prepare for Testing
295295
run: |
296296
echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_ENV
297+
# Visual tests must run against the nginx-served Docker artifact (with the
298+
# production CSP/security headers), not the bare Vite dev server. Start the
299+
# container on :3000 so Playwright's webServer (reuseExistingServer) reuses
300+
# it instead of spinning up `npm start`. Otherwise CSP regressions (e.g. a
301+
# missing connect-src breaking MSW-mocked stories) go undetected.
302+
- name: Download Docker image artifact
303+
uses: actions/download-artifact@v8
304+
with:
305+
name: docker-image
306+
path: /tmp
307+
- name: Load Docker image
308+
run: docker load -i /tmp/docker-image.tar
309+
- name: Start application container
310+
run: docker run -d -p 3000:80 ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }}
297311
- uses: actions/setup-node@v6
298312
with:
299313
node-version: 24

nginx-default.conf

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
11
server {
22
listen 80;
33
server_name localhost;
4+
server_tokens off; # don't leak the nginx version in the Server header / error pages
5+
6+
# Two locations serve from the same root with near-identical security headers. nginx
7+
# does not inherit add_header into a location that sets its own, so every header is
8+
# repeated in both blocks — keep them in sync.
9+
10+
# Storybook (built to /storybook). Headers mirror the app block below; its CSP differs
11+
# only where Storybook needs it (see the directive notes on the CSP line). Declared
12+
# before `location /` so it matches first.
13+
location /storybook/ {
14+
root /usr/share/nginx/html;
15+
try_files $uri $uri/ /storybook/index.html;
16+
17+
add_header X-Content-Type-Options nosniff always; # block MIME sniffing
18+
add_header X-Frame-Options SAMEORIGIN always; # legacy clickjacking guard (see frame-ancestors)
19+
add_header X-XSS-Protection 0 always; # deprecated; OWASP recommends disabling
20+
add_header Referrer-Policy strict-origin-when-cross-origin always;
21+
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always; # isolate the browsing context, keep popups
22+
add_header Permissions-Policy "camera=(), microphone=(), payment=(), usb=(), browsing-topics=()" always; # disable unused features + Topics API
23+
# CSP — the app block below documents each directive; Storybook differs only in:
24+
# - script-src-elem 'unsafe-inline': Storybook's built HTML inlines its bootstrap <script>s.
25+
# - connect-src 'self' *.hasadna.org.il: MSW-mocked stories fetch the API (CSP is checked before the service worker intercepts).
26+
# - no GA/GTM hosts: Storybook runs no analytics.
27+
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; script-src-elem 'self' 'unsafe-inline'; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.openstreetmap.fr https://*.openstreetmap.org; connect-src 'self' https://*.hasadna.org.il; frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com; worker-src 'self' blob:; frame-ancestors 'self';" always;
28+
}
29+
430
location / {
531
root /usr/share/nginx/html;
632
index index.html index.htm;
733
try_files $uri /index.html;
34+
35+
add_header X-Content-Type-Options nosniff always; # block MIME sniffing
36+
add_header X-Frame-Options SAMEORIGIN always; # legacy clickjacking guard (see frame-ancestors)
37+
add_header X-XSS-Protection 0 always; # deprecated; OWASP recommends disabling
38+
add_header Referrer-Policy strict-origin-when-cross-origin always;
39+
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always; # isolate the browsing context, keep _blank/window.open links
40+
add_header Permissions-Policy "camera=(), microphone=(), payment=(), usb=(), browsing-topics=()" always; # disable unused features + Topics API (geolocation/clipboard/fullscreen/autoplay stay default-allowed)
41+
# CSP, per directive:
42+
# - script-src: no 'unsafe-inline'/'unsafe-eval' (built index.html has no inline scripts); GA/GTM hosts load the analytics scripts.
43+
# - style-src-elem 'unsafe-inline': MUI/emotion and styled-components inject runtime <style> elements. style-src omits
44+
# 'unsafe-inline', so inline style="" attributes stay blocked (the app sets none — React style={{}} writes via the CSSOM).
45+
# - img-src: openstreetmap tiles, google-analytics beacon pixels.
46+
# - connect-src: *.hasadna.org.il for the stride/backend APIs, google-analytics/googletagmanager for analytics.
47+
# - frame-src: youtube(-nocookie).com instruction videos, docs.google.com hackathon registration form (renders while POSTPONED=true).
48+
# - frame-ancestors 'self': CSP3 clickjacking control; same effect as the X-Frame-Options SAMEORIGIN above.
49+
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.openstreetmap.fr https://*.openstreetmap.org https://www.google-analytics.com; connect-src 'self' https://*.hasadna.org.il https://www.google-analytics.com https://region1.google-analytics.com https://www.googletagmanager.com; frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com https://docs.google.com; worker-src 'self' blob:; frame-ancestors 'self';" always;
850
}
951
}

0 commit comments

Comments
 (0)