security: remove style-src 'unsafe-inline' from all CSP policies (#197)
Closes #193 Migrate three inline-style React props to CSS classes / CSSOM: - ErrorExpansion: cursor:copy and copy-hint color moved to BEM classes - SegmentedControl: dynamic transform now driven by --ec-segment-offset CSS var, set via useLayoutEffect + ref.style.setProperty Remove 'unsafe-inline' from style-src in all three enforcement layers: - vite.config.web.ts (prod only; dev keeps it for HMR) - nginx.conf (both CSP directives) - public/_headers (Cloudflare Pages) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4efdd770dd
commit
1c642f5b37
7 changed files with 25 additions and 16 deletions
|
|
@ -23,7 +23,7 @@ http {
|
|||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||
|
||||
# Cache static assets (hashed filenames) for 1 year
|
||||
location /assets/ {
|
||||
|
|
@ -32,7 +32,7 @@ http {
|
|||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||
}
|
||||
|
||||
# Service worker — no cache (must always be fresh)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Referrer-Policy: no-referrer
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; frame-ancestors 'none'
|
||||
Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'; base-uri 'none'; frame-ancestors 'none'
|
||||
|
||||
# Hashed assets are content-addressed; cache aggressively
|
||||
/assets/*
|
||||
|
|
|
|||
|
|
@ -57,18 +57,12 @@ export function ErrorExpansion({
|
|||
return (
|
||||
<div className="file-table__expansion">
|
||||
<pre
|
||||
className="file-table__error-text"
|
||||
className="file-table__error-text file-table__error-text--copyable"
|
||||
onClick={handleCopy}
|
||||
style={{ cursor: "copy" }}
|
||||
>
|
||||
{error}
|
||||
</pre>
|
||||
<span
|
||||
className="file-table__copy-hint"
|
||||
style={{ color: "var(--ec-color-text-secondary)" }}
|
||||
>
|
||||
Click to copy
|
||||
</span>
|
||||
<span className="file-table__copy-hint">Click to copy</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useLayoutEffect, useRef } from "react";
|
||||
import "../../styles/segmented_control.css";
|
||||
|
||||
interface SegmentOption<T extends string> {
|
||||
|
|
@ -18,6 +19,14 @@ export function SegmentedControl<const T extends string>({
|
|||
label: string;
|
||||
}): React.JSX.Element {
|
||||
const activeIndex = options.findIndex((o) => o.value === value);
|
||||
const indicatorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
indicatorRef.current?.style.setProperty(
|
||||
"--ec-segment-offset",
|
||||
`${activeIndex * 100}%`,
|
||||
);
|
||||
}, [activeIndex]);
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent, index: number): void {
|
||||
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||||
|
|
@ -54,10 +63,7 @@ export function SegmentedControl<const T extends string>({
|
|||
<span className="segmented-control__label">{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
className="segmented-control__indicator"
|
||||
style={{ transform: `translateX(${activeIndex * 100}%)` }}
|
||||
/>
|
||||
<div ref={indicatorRef} className="segmented-control__indicator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,14 @@
|
|||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.file-table__error-text--copyable {
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.file-table__copy-hint {
|
||||
color: var(--ec-color-text-secondary);
|
||||
}
|
||||
|
||||
.file-table__error-link {
|
||||
color: var(--ec-color-accent);
|
||||
word-break: break-all;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
z-index: 0;
|
||||
transform: translateX(var(--ec-segment-offset, 0%));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function webCspPlugin(): Plugin {
|
|||
const scriptSrc = isDev
|
||||
? "'self' 'unsafe-inline' 'wasm-unsafe-eval'"
|
||||
: "'self' 'wasm-unsafe-eval'";
|
||||
const styleSrc = "'self' 'unsafe-inline'";
|
||||
const styleSrc = isDev ? "'self' 'unsafe-inline'" : "'self'";
|
||||
const connectSrc = isDev ? "'self' ws://localhost:*" : "'self'";
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue