Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 31 additions & 29 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ const imageUrl = new URL(image, SITE_ORIGIN).toString();
const cloudflareBeaconToken = import.meta.env.PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKEN;
const googleAnalyticsId = import.meta.env.PUBLIC_GOOGLE_ANALYTICS_ID;
const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
const googleAnalyticsScript = googleAnalyticsId
? `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', ${JSON.stringify(googleAnalyticsId)});`
: '';

const siteUrl = new URL('/', SITE_ORIGIN).toString();
const organizationId = `${siteUrl}#organization`;
Expand Down Expand Up @@ -100,10 +106,9 @@ const jsonLdString = JSON.stringify(structuredData).replace(/</g, '\\u003c');
*
* This is a static (SSG) site with no server able to set per-request response
* headers or nonces, so the policy is delivered via a `<meta http-equiv>` tag.
* Inline `<script>` is forbidden (`script-src 'self'`); the only inline script
* is the JSON-LD block above, which we allow by its exact SHA-256 hash rather
* than `'unsafe-inline'`, so an injected `<script>` in user-derived HTML (e.g.
* the sanitised blog body) still cannot execute.
* Inline scripts are forbidden unless their exact SHA-256 hash is allow-listed.
* This covers JSON-LD and the optional Google Analytics bootstrap without
* allowing arbitrary scripts from user-derived HTML.
*
* `style-src 'self'` with NO `'unsafe-inline'`: Astro emits all CSS (scoped
* component styles + Tailwind) as external stylesheets, and the one inline
Expand All @@ -114,20 +119,23 @@ const jsonLdString = JSON.stringify(structuredData).replace(/</g, '\\u003c');
// SHA-256 of the exact JSON-LD bytes, base64-encoded, for the CSP allow-list.
// Uses the Web Crypto API (available in Astro's build runtime) so no Node-only
// types are required. Top-level `await` is supported in Astro frontmatter.
const jsonLdDigest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(jsonLdString)
);
let jsonLdBinary = '';
for (const byte of new Uint8Array(jsonLdDigest)) {
jsonLdBinary += String.fromCharCode(byte);
async function sha256Base64(value: string) {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
let binary = '';
for (const byte of new Uint8Array(digest)) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
}
const jsonLdHash = btoa(jsonLdBinary);
const jsonLdHash = await sha256Base64(jsonLdString);
const googleAnalyticsHash = googleAnalyticsScript
? await sha256Base64(googleAnalyticsScript)
: '';
const styleSrc = import.meta.env.DEV ? "style-src 'self' 'unsafe-inline'" : "style-src 'self'";
const csp = [
"default-src 'self'",
"base-uri 'self'",
`script-src 'self' 'sha256-${jsonLdHash}'${cloudflareBeaconToken ? ' https://static.cloudflareinsights.com' : ''}${googleAnalyticsId ? ' https://www.googletagmanager.com' : ''}`,
`script-src 'self' 'sha256-${jsonLdHash}'${googleAnalyticsHash ? ` 'sha256-${googleAnalyticsHash}' https://*.googletagmanager.com` : ''}${cloudflareBeaconToken ? ' https://static.cloudflareinsights.com' : ''}`,
styleSrc,
// First-party + GitHub avatar/raw image hosts only — no third-party beacons.
`img-src 'self' data: https://raw.githubusercontent.com https://avatars.githubusercontent.com${googleAnalyticsId ? ' https://*.google-analytics.com https://*.googletagmanager.com' : ''}`,
Expand Down Expand Up @@ -169,6 +177,16 @@ const csp = [
<link rel="icon" href={`${baseUrl}brand/favicon.ico`} sizes="any" />
<link rel="icon" href={`${baseUrl}brand/favicon-96.png`} type="image/png" sizes="96x96" />
<link rel="apple-touch-icon" href={`${baseUrl}brand/apple-touch-icon.png`} />
{
googleAnalyticsId && (
<script
is:inline
async
src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(googleAnalyticsId)}`}
></script>
)
}
{googleAnalyticsScript && <script is:inline set:html={googleAnalyticsScript} />}
<meta property="og:site_name" content="IndopenSource" />
<meta property="og:locale" content="id_ID" />
<meta property="og:type" content={type} />
Expand Down Expand Up @@ -258,22 +276,6 @@ const csp = [
document.head.append(beacon);
}

const googleAnalyticsId = import.meta.env.PUBLIC_GOOGLE_ANALYTICS_ID;
if (googleAnalyticsId) {
const tag = document.createElement('script');
tag.async = true;
tag.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(googleAnalyticsId)}`;
document.head.append(tag);

const analyticsWindow = window as typeof window & {
dataLayer?: unknown[][];
gtag?: (...args: unknown[]) => void;
};
const dataLayer = analyticsWindow.dataLayer ||= [];
const gtag = analyticsWindow.gtag ||= (...args: unknown[]) => dataLayer.push(args);
gtag('js', new Date());
gtag('config', googleAnalyticsId);
}
</script>
</body>
</html>