From 94d40f0c13e66a89ed60111d41cc077a7d808f57 Mon Sep 17 00:00:00 2001 From: 24c02 <163450896+24c02@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:51:14 -0400 Subject: [PATCH] enable strict CSP with nonces, eliminate all inline event handlers - activate Content-Security-Policy with nonces for scripts/styles - allowlist external domains: jsdelivr (fonts), googleapis (maps), hackclub CDNs (images), are-we-there-yet (iframe) - no unsafe-inline for scripts in any environment - dev: unsafe-inline for styles (vite HMR), vite host for scripts/ws convert every onclick/onsubmit/onerror attribute in ERB and Phlex to data-* attributes with delegated addEventListener handlers: views: backend/identities, backend/verifications, backend/static_pages, backend/verification_cases, backend/shared/_review_document_files, identities/new, developer_apps/show, docs/show, static_pages/slack_id, error pages, layouts/backend components: backup_codes_display, window, code_input, sidebar, banner add nonce to all inline <% else %> no files attached <% end %> diff --git a/app/views/backend/static_pages/index.html.erb b/app/views/backend/static_pages/index.html.erb index 598f2ba3..ed6e6dcc 100644 --- a/app/views/backend/static_pages/index.html.erb +++ b/app/views/backend/static_pages/index.html.erb @@ -27,6 +27,7 @@ <%= render_nav_item backend_verification_cases_path, "Manual call cases", "CASE" %> <%= render_nav_item qa_backend_verification_cases_path, "QA sampling", "QASM" %> <%= render_nav_item backend_persona_dashboard_path, "Persona rollout", "PRSA" %> + <%= internal_content(:ember_glass_hollow) %> <% end %> @@ -44,7 +45,7 @@
- + Go anywhere ⌘K @@ -58,16 +59,16 @@ - - + + navigate - + open - + search diff --git a/app/views/backend/verification_cases/show.html.erb b/app/views/backend/verification_cases/show.html.erb index 7160d49f..acff251b 100644 --- a/app/views/backend/verification_cases/show.html.erb +++ b/app/views/backend/verification_cases/show.html.erb @@ -66,11 +66,10 @@ alt: "case document #{index + 1}", id: "case-image-#{index}", style: "max-width: 100%; transition: transform 0.2s;", - data: { rotation: 0 }, - onload: "document.getElementById('case-loader-#{index}').style.display='none'; document.getElementById('case-image-container-#{index}').style.display='block';", - onerror: "document.getElementById('case-loader-#{index}').textContent='error loading image';" %> + data: { rotation: 0, loader: "case-loader-#{index}", container: "case-image-container-#{index}" }, + class: "case-doc-image" %> - + <%= link_to "open ↗", url_for(file), target: "_blank" %> @@ -96,15 +95,6 @@ <% end %> - <% else %> nothing submitted yet <% end %> diff --git a/app/views/backend/verifications/show.html.erb b/app/views/backend/verifications/show.html.erb index ec457fa1..e2794445 100644 --- a/app/views/backend/verifications/show.html.erb +++ b/app/views/backend/verifications/show.html.erb @@ -245,7 +245,7 @@ <%= form.text_area :internal_rejection_comment, rows: 2, placeholder: "internal notes (not visible to user)..." %> - + <% end %> diff --git a/app/views/developer_apps/show.html.erb b/app/views/developer_apps/show.html.erb index 758ed21e..68d57e9a 100644 --- a/app/views/developer_apps/show.html.erb +++ b/app/views/developer_apps/show.html.erb @@ -52,15 +52,15 @@

<%= t(".danger_zone") %>

<% if policy(@app).rotate_credentials? %> <%= button_to t(".rotate_credentials"), rotate_credentials_developer_app_path(@app), method: :post, class: "danger small-btn", - form: { onsubmit: "return confirm('#{j t('.rotate_confirm')}')" } %> + form: { data: { confirm: t('.rotate_confirm') } } %> <% end %> <% if policy(@app).revoke_all_authorizations? %> <%= button_to t(".revoke_all_authorizations"), revoke_all_authorizations_developer_app_path(@app), method: :post, class: "danger small-btn", - form: { onsubmit: "return confirm('#{j t('.revoke_all_authorizations_confirm')}')" } %> + form: { data: { confirm: t('.revoke_all_authorizations_confirm') } } %> <% end %> <% if policy(@app).destroy? %> <%= button_to t(".delete_app"), developer_app_path(@app), method: :delete, class: "danger small-btn", - form: { onsubmit: "return confirm('#{j t('.delete_confirm')}')" } %> + form: { data: { confirm: t('.delete_confirm') } } %> <% end %> <% end %> @@ -144,7 +144,7 @@ <%= button_to t(".remove_collaborator"), developer_app_collaborator_path(@app, collab), method: :delete, class: "danger small-btn", - form: { onsubmit: "return confirm('#{j t('.remove_collaborator_confirm')}')" } %> + form: { data: { confirm: t('.remove_collaborator_confirm') } } %> <% end %> <% else %> diff --git a/app/views/docs/show.html.erb b/app/views/docs/show.html.erb index e4e6dc92..bc1b859d 100644 --- a/app/views/docs/show.html.erb +++ b/app/views/docs/show.html.erb @@ -1,14 +1,14 @@ <% content_for :title, @doc[:title] %>
- -
+
- diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb index 1bcca8ed..616e6a27 100644 --- a/app/views/doorkeeper/authorizations/new.html.erb +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -82,7 +82,7 @@ <% end %> <% auth_label = application&.hq_official? ? t('.authorize_official') : t('.authorize') %> -
' }" x-init="if (countdown > 0) { let interval = setInterval(() => { countdown--; if (countdown <= 0) { ready = true; clearInterval(interval); } }, 1000); }"> +
<%= form_tag oauth_authorization_path, method: :delete do %> <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %> @@ -106,7 +106,7 @@ <%= hidden_field_tag :nonce, @pre_auth.nonce, id: nil %> <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %> <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %> - <%= submit_tag auth_label, "x-bind:disabled": "!ready", "x-bind:value": "ready ? authLabel : `Wait ${countdown}s...`", class: "approve" %> + <%= submit_tag auth_label, "x-bind:disabled": "!ready", "x-bind:value": "buttonLabel", class: "approve" %> <% end %>
diff --git a/app/views/errors/internal_server_error.html.erb b/app/views/errors/internal_server_error.html.erb index 281cf32e..87abb798 100644 --- a/app/views/errors/internal_server_error.html.erb +++ b/app/views/errors/internal_server_error.html.erb @@ -90,7 +90,6 @@ <%= @event_id %> diff --git a/app/views/errors/not_found.html.erb b/app/views/errors/not_found.html.erb index 7917179d..659e6050 100644 --- a/app/views/errors/not_found.html.erb +++ b/app/views/errors/not_found.html.erb @@ -88,7 +88,6 @@ <%= @event_id %> diff --git a/app/views/errors/unprocessable_entity.html.erb b/app/views/errors/unprocessable_entity.html.erb index 1faa289e..87d17a3a 100644 --- a/app/views/errors/unprocessable_entity.html.erb +++ b/app/views/errors/unprocessable_entity.html.erb @@ -88,7 +88,6 @@ <%= @event_id %> diff --git a/app/views/identities/new.html.erb b/app/views/identities/new.html.erb index 14465ea9..e2bc6af2 100644 --- a/app/views/identities/new.html.erb +++ b/app/views/identities/new.html.erb @@ -36,24 +36,11 @@ <%= t(".email_typo_html", suggested: @identity.suggested_email) %> + + diff --git a/app/views/identity_totps/show.html.erb b/app/views/identity_totps/show.html.erb index 764d56d7..6bf40e28 100644 --- a/app/views/identity_totps/show.html.erb +++ b/app/views/identity_totps/show.html.erb @@ -1,9 +1,4 @@ -
+

<%= t(".heading") %>

<%= t(".step_indicator") %>

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 26b87952..216d5394 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -8,7 +8,7 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= yield :head %> - diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 0b319544..37491609 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -4,31 +4,28 @@ # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.configure do -# config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# Allow @vite/client to hot reload javascript changes in development -# policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development? +Rails.application.configure do + config.content_security_policy do |policy| + policy.default_src :self + policy.font_src :self, "https://cdn.jsdelivr.net" + policy.img_src :self, :data, :blob, "https://cdn.hackclub.com", "https://hc-cdn.hel1.your-objectstorage.com", "https://user-cdn.hackclub-assets.com" + policy.object_src :none + policy.script_src :self, "https://maps.googleapis.com" + policy.style_src :self + policy.frame_src :self, "https://are-we-there-yet.hackclub.com", "https://*.withpersona.com" + policy.connect_src :self, "https://maps.googleapis.com", "https://*.withpersona.com" -# You may need to enable this in production as well depending on your setup. -# policy.script_src *policy.script_src, :blob if Rails.env.test? + if Rails.env.development? + policy.script_src *policy.script_src, "http://#{ViteRuby.config.host_with_port}" + policy.style_src *policy.style_src, :unsafe_inline + policy.connect_src *policy.connect_src, "ws://#{ViteRuby.config.host_with_port}" + end -# policy.style_src :self, :https -# Allow @vite/client to hot reload style changes in development -# policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development? + if Rails.env.test? + policy.script_src *policy.script_src, :blob + end + end -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end -# -# # Generate session nonces for permitted importmap, inline scripts, and inline styles. -# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } -# config.content_security_policy_nonce_directives = %w(script-src style-src) -# -# # Report violations without enforcing the policy. -# # config.content_security_policy_report_only = true -# end + config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) } + config.content_security_policy_nonce_directives = %w[script-src style-src] +end diff --git a/package.json b/package.json index 117164d0..3e54ddae 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "vite-plugin-ruby": "^5.1.1" }, "dependencies": { + "@alpinejs/csp": "^3.17.5", "@fingerprintjs/fingerprintjs": "^3.4.2", "@noble/curves": "^1.9.1", "@picocss/pico": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index 8ca7d075..1985a62e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@alpinejs/csp@^3.17.5": + version "3.17.5" + resolved "https://registry.yarnpkg.com/@alpinejs/csp/-/csp-3.17.5.tgz#a8e707bd979e587aec4c477f0d7fba53c935c653" + integrity sha512-7e5lYgzJJL2eZ7esnx+E7wtxws+VDs9em9ztttZ+AQAYEng2bABHFp2SUga0wt84RyfL7hc6B+S/RGglXsPf6w== + dependencies: + "@vue/reactivity" "~3.5.40" + "@bufbuild/protobuf@^2.0.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.4.0.tgz#ef6bfd2fc0374a95a4904cdc5fd8e02cc999cf73" @@ -598,11 +605,23 @@ dependencies: "@vue/shared" "3.1.5" +"@vue/reactivity@~3.5.40": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.44.tgz#6721374ab041003d16f2a90304f720c3b35d568c" + integrity sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw== + dependencies: + "@vue/shared" "3.5.44" + "@vue/shared@3.1.5": version "3.1.5" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.1.5.tgz#74ee3aad995d0a3996a6bb9533d4d280514ede03" integrity sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA== +"@vue/shared@3.5.44": + version "3.5.44" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.44.tgz#eefe91aa2e17c617260c4ae4c21246be5d85642e" + integrity sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg== + "@webtui/css@^0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@webtui/css/-/css-0.1.7.tgz#9fe53161f387e3b50e6012dbacec17a2da2910b7"