Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ exports[`<NavBarNavList /> renders unchanged 1`] = `
class="css-1gfen40"
>
<a
aria-label="Facebook"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1iki3ku-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://www.facebook.com/CodeForAfrica"
rel="noreferrer noopener"
Expand All @@ -79,6 +80,7 @@ exports[`<NavBarNavList /> renders unchanged 1`] = `
class="css-1gfen40"
>
<a
aria-label="Twitter"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1iki3ku-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://twitter.com/Code4Africa"
rel="noreferrer noopener"
Expand Down
5 changes: 5 additions & 0 deletions apps/climatemappedafrica/src/components/Layout/Layout.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ exports[`<Layout /> renders unchanged 1`] = `
class="MuiStack-root css-1d9cypr-MuiStack-root"
>
<a
aria-label="Facebook"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1bo9crh-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://www.google.com"
id="67050ad20d400b5e511b2871"
Expand Down Expand Up @@ -284,6 +285,7 @@ exports[`<Layout /> renders unchanged 1`] = `
class="MuiStack-root css-1rq54rq-MuiStack-root"
>
<a
aria-label="Facebook"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1ivk23z-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://www.google.com"
id="67050ad20d400b5e511b2871"
Expand All @@ -301,6 +303,7 @@ exports[`<Layout /> renders unchanged 1`] = `
</svg>
</a>
<a
aria-label="Twitter"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1ivk23z-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://Twitter.com"
id="6706844a6339b76017e6c782"
Expand All @@ -318,6 +321,7 @@ exports[`<Layout /> renders unchanged 1`] = `
</svg>
</a>
<a
aria-label="Github"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1ivk23z-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://github.com"
id="670684516339b76017e6c783"
Expand All @@ -335,6 +339,7 @@ exports[`<Layout /> renders unchanged 1`] = `
</svg>
</a>
<a
aria-label="Instagram"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-1ivk23z-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://instagram.com"
id="6706845b6339b76017e6c784"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ exports[`<NavBarNavList /> renders unchanged 1`] = `
class="css-1gfen40"
>
<a
aria-label="Facebook"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-k5xgb-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://www.facebook.com/CodeForAfrica"
rel="noreferrer noopener"
Expand All @@ -79,6 +80,7 @@ exports[`<NavBarNavList /> renders unchanged 1`] = `
class="css-1gfen40"
>
<a
aria-label="Twitter"
class="MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways MuiTypography-root MuiTypography-inherit MuiLink-root MuiLink-underlineAlways css-k5xgb-MuiTypography-root-MuiLink-root-MuiTypography-root-MuiLink-root"
href="https://twitter.com/Code4Africa"
rel="noreferrer noopener"
Expand Down
6 changes: 6 additions & 0 deletions apps/trustlab/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { withPayload } from "@payloadcms/next/withPayload";
import { fileURLToPath } from "url";
import path from "path";

import { securityHeaders } from "./src/utils/securityHeaders.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Expand Down Expand Up @@ -63,6 +65,10 @@ const nextConfig = {
],
async headers() {
return [
{
source: "/:path*",
headers: securityHeaders,
},
{
source: "/api/media/file/:path*",
headers: [
Expand Down
3 changes: 2 additions & 1 deletion apps/trustlab/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"require-in-the-middle": "catalog:payload-v3",
"sharp": "catalog:",
"swr": "catalog:",
"validate-color": "catalog:"
"validate-color": "catalog:",
"xss": "catalog:"
},
"devDependencies": {
"@commons-ui/testing-library": "workspace:*",
Expand Down
20 changes: 10 additions & 10 deletions apps/trustlab/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ import { defaultLocale, locales } from "@/trustlab/payload/utils/locales";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const defaultAllowedOrigin = site.url.replace(/\/+$/, "");

const cors =
process.env.PAYLOAD_CORS?.split(",")
const normalizeOrigins = (value?: string) =>
value
?.split(",")
.map((d) => d.trim())
.map((d) => d.replace(/\/+$/, ""))
.filter(Boolean) ?? [];

const csrf =
process.env.PAYLOAD_CSRF?.split(",")
.map((d) => d.trim())
.filter(Boolean) ?? [];
const cors = normalizeOrigins(process.env.PAYLOAD_CORS);
const csrf = normalizeOrigins(process.env.PAYLOAD_CSRF);

let nodemailerAdapterArgs: NodemailerAdapterArgs | undefined;
if (process.env.SMTP_HOST && process.env.SMTP_PASS) {
Expand Down Expand Up @@ -87,8 +88,8 @@ export default buildConfig({
Users,
Opportunities,
] as CollectionConfig[],
cors,
csrf,
cors: cors.length ? cors : [defaultAllowedOrigin],
csrf: csrf.length ? csrf : [defaultAllowedOrigin],
db: mongooseAdapter({
url: process.env.DATABASE_URL ?? false,
}),
Expand All @@ -106,8 +107,7 @@ export default buildConfig({
: undefined),
plugins: [...plugins],
secret: process.env.PAYLOAD_SECRET || "",
// Just the origin, without trailing slash.
serverURL: site.url.slice(0, -1),
serverURL: defaultAllowedOrigin,
sharp,
telemetry: process.env.NEXT_TELEMETRY_DISABLED === "0",
typescript: {
Expand Down
6 changes: 4 additions & 2 deletions apps/trustlab/src/components/ActionBanner/ActionBanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import React, { forwardRef, useState } from "react";

import CloseIcon from "@/trustlab/assets/icons/close.svg";
import Button from "@/trustlab/components/StyledButton";
import sanitizeEmbedHtml from "@/trustlab/utils/sanitizeEmbedHtml";

const ActionBanner = forwardRef(function ActionBanner(
{
Expand All @@ -28,7 +29,8 @@ const ActionBanner = forwardRef(function ActionBanner(
},
ref,
) {
const hasEmbed = Boolean(embedCode);
const sanitizedEmbedCode = sanitizeEmbedHtml(embedCode);
const hasEmbed = Boolean(sanitizedEmbedCode);
const [open, setOpen] = useState(false);

const handleOpen = () => {
Expand Down Expand Up @@ -145,7 +147,7 @@ const ActionBanner = forwardRef(function ActionBanner(
</DialogTitle>
<DialogContent>
<Box
dangerouslySetInnerHTML={{ __html: embedCode }}
dangerouslySetInnerHTML={{ __html: sanitizedEmbedCode }}
sx={{
width: "100%",
"& iframe": {
Expand Down
7 changes: 5 additions & 2 deletions apps/trustlab/src/components/HelplineCard/HelplineCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import React, { useState } from "react";

import HelplineEmbedDialog from "./HelplineEmbedDialog";

import sanitizeEmbedHtml from "@/trustlab/utils/sanitizeEmbedHtml";

function HelplineCard({
title,
icon: media,
Expand All @@ -23,7 +25,8 @@ function HelplineCard({
embedButtonLabel,
embedCloseLabel,
}) {
const hasEmbed = Boolean(embedCode);
const sanitizedEmbedCode = sanitizeEmbedHtml(embedCode);
const hasEmbed = Boolean(sanitizedEmbedCode);
const [open, setOpen] = useState(false);
const buttonLabel = embedButtonLabel || link?.label || title;

Expand Down Expand Up @@ -124,7 +127,7 @@ function HelplineCard({
</CardActions>
<HelplineEmbedDialog
closeLabel={embedCloseLabel}
embedCode={embedCode}
embedCode={sanitizedEmbedCode}
onClose={handleClose}
open={open}
title={title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import React from "react";

function HelplineEmbedDialog({ closeLabel, embedCode, onClose, open, title }) {
const dialogTitleId = React.useId();
const hasEmbed = Boolean(embedCode);

return (
<Dialog
aria-labelledby={dialogTitleId}
fullWidth
maxWidth={false}
onClose={onClose}
open={Boolean(embedCode && open)}
open={Boolean(hasEmbed && open)}
PaperProps={{
sx: {
width: "100%",
Expand Down Expand Up @@ -47,7 +48,7 @@ function HelplineEmbedDialog({ closeLabel, embedCode, onClose, open, title }) {
},
}}
dangerouslySetInnerHTML={
embedCode
hasEmbed
? {
__html: embedCode,
}
Expand Down
6 changes: 4 additions & 2 deletions apps/trustlab/src/components/RowCard/RowCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useState, forwardRef } from "react";
import RowCardActionButton from "./RowCardActionButton";

import HelplineEmbedDialog from "@/trustlab/components/HelplineCard/HelplineEmbedDialog";
import sanitizeEmbedHtml from "@/trustlab/utils/sanitizeEmbedHtml";

const RowCard = forwardRef(function RowCard(props, ref) {
const {
Expand All @@ -20,7 +21,8 @@ const RowCard = forwardRef(function RowCard(props, ref) {
embedCloseLabel,
...other
} = props;
const hasEmbed = Boolean(embedCode);
const sanitizedEmbedCode = sanitizeEmbedHtml(embedCode);
const hasEmbed = Boolean(sanitizedEmbedCode);
const [open, setOpen] = useState(false);
const buttonLabel = embedButtonLabel || actionLabel;

Expand Down Expand Up @@ -114,7 +116,7 @@ const RowCard = forwardRef(function RowCard(props, ref) {
</Stack>
<HelplineEmbedDialog
closeLabel={embedCloseLabel}
embedCode={embedCode}
embedCode={sanitizedEmbedCode}
onClose={handleClose}
open={open}
title={title}
Expand Down
37 changes: 37 additions & 0 deletions apps/trustlab/src/utils/sanitizeEmbedHtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { FilterXSS } from "xss";

const embedSanitizer = new FilterXSS({
whiteList: {
a: ["href", "target", "rel", "aria-label"],
br: [],
div: ["class"],
iframe: [
"allow",
"allowfullscreen",
"class",
"frameborder",
"height",
"loading",
"name",
"referrerpolicy",
"sandbox",
"scrolling",
"src",
"title",
"width",
],
p: ["class"],
section: ["class"],
span: ["class"],
},
stripIgnoreTag: true,
stripIgnoreTagBody: ["script", "style"],
});

export default function sanitizeEmbedHtml(html) {
if (!html?.trim()) {
return "";
}

return embedSanitizer.process(html);
}
67 changes: 67 additions & 0 deletions apps/trustlab/src/utils/securityHeaders.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
function getContentSecurityPolicy(nodeEnv) {
const isProduction = nodeEnv === "production";
const connectSrc = ["'self'", "https:"];
const scriptSrc = ["'self'", "'unsafe-inline'", "https:"];

// Next.js dev tooling relies on websocket connections and eval-based refresh
// helpers. Keep those allowances scoped to non-production environments.
if (!isProduction) {
connectSrc.push("http:", "ws:", "wss:");
scriptSrc.splice(1, 0, "'unsafe-eval'");
}

return [
"default-src 'self'",
"base-uri 'self'",
`connect-src ${connectSrc.join(" ")}`,
"font-src 'self' data: https:",
"form-action 'self'",
"frame-ancestors 'self'",
"frame-src 'self' https:",
"img-src 'self' data: blob: https:",
"media-src 'self' blob: https:",
"object-src 'none'",
// Inline script/style support is still required by the current app and MUI
// setup, so this policy tightens sources without breaking rendering.
`script-src ${scriptSrc.join(" ")}`,
"style-src 'self' 'unsafe-inline' https:",
].join("; ");
}

export function getSecurityHeaders(nodeEnv = process.env.NODE_ENV) {
const headers = [
{
key: "Content-Security-Policy",
value: getContentSecurityPolicy(nodeEnv),
},
{
key: "Permissions-Policy",
value: "camera=(), geolocation=(), microphone=(), payment=(), usb=()",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
];

if (nodeEnv === "production") {
// HSTS is intentionally production-only so local HTTP development and
// non-TLS preview setups do not get sticky HTTPS behavior.
headers.push({
key: "Strict-Transport-Security",
value: "max-age=31536000; includeSubDomains; preload",
});
}

return headers;
}

export const securityHeaders = getSecurityHeaders();
Loading
Loading