Commit 4a32e7de authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #126 from blockscout/csp

csp and some security
parents e564b95f 09c79f4a
......@@ -4,6 +4,8 @@ NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_ORG=block-scout
SENTRY_PROJECT=new-ui
SENTRY_AUTH_TOKEN=xxx
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_BLOCKSCOUT_VERSION=xxx
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
......
const getCspPolicy = require('../../lib/csp/getCspPolicy');
async function headers() {
return [
{
source: '/:path*',
headers: [
// security headers from here - https://nextjs.org/docs/advanced-features/security-headers
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Content-Security-Policy-Report-Only',
value: getCspPolicy(),
},
],
},
];
}
module.exports = headers;
const parseNetworkConfig = require('../networks/parseNetworkConfig');
const KEY_WORDS = {
BLOB: 'blob:',
DATA: 'data:',
NONE: '\'none\'',
REPORT_SAMPLE: `'report-sample'`,
SELF: '\'self\'',
STRICT_DYNAMIC: `'strict-dynamic'`,
UNSAFE_INLINE: '\'unsafe-inline\'',
UNSAFE_EVAL: '\'unsafe-eval\'',
};
const MAIN_DOMAINS = [ '*.blockscout.com', 'blockscout.com' ];
const isDev = process.env.NODE_ENV === 'development';
function getNetworksExternalAssets() {
const icons = parseNetworkConfig()
.filter(({ icon }) => typeof icon === 'string')
.map(({ icon }) => new URL(icon));
return icons;
}
function makePolicyMap() {
const networkExternalAssets = getNetworksExternalAssets();
return {
'default-src': [
KEY_WORDS.NONE,
],
'connect-src': [
KEY_WORDS.SELF,
// webpack hmr in safari doesn't recognize localhost as 'self' for some reason
isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// client error monitoring
'sentry.io', '*.sentry.io',
],
'script-src': [
KEY_WORDS.SELF,
// next.js generates and rebuilds source maps in dev using eval()
// https://github.com/vercel/next.js/issues/14221#issuecomment-657258278
isDev ? KEY_WORDS.UNSAFE_EVAL : '',
...MAIN_DOMAINS,
// hash of ColorModeScript
'\'sha256-e7MRMmTzLsLQvIy1iizO1lXf7VWYoQ6ysj5fuUzvRwE=\'',
],
'style-src': [
KEY_WORDS.SELF,
...MAIN_DOMAINS,
// google fonts
'fonts.googleapis.com',
// yes, it is unsafe as it stands, but
// - we cannot use hashes because all styles are generated dynamically
// - we cannot use nonces since we are not following along SSR path
// - and still there is very small damage that can be cause by CSS-based XSS-attacks
// so we hope we are fine here till the first major incident :)
KEY_WORDS.UNSAFE_INLINE,
],
'img-src': [
KEY_WORDS.SELF,
KEY_WORDS.DATA,
...MAIN_DOMAINS,
// github avatars
'avatars.githubusercontent.com',
// network assets
...networkExternalAssets.map((url) => url.host),
],
'font-src': [
KEY_WORDS.DATA,
// google fonts
'*.gstatic.com',
'fonts.googleapis.com',
],
'object-src': [
KEY_WORDS.NONE,
],
'base-uri': [
KEY_WORDS.NONE,
],
'report-uri': [
process.env.SENTRY_CSP_REPORT_URI,
],
};
}
function getCspPolicy() {
const policyMap = makePolicyMap();
const policyString = Object.entries(policyMap)
.map(([ key, value ]) => {
if (!value || value.length === 0) {
return;
}
return [ key, value.join(' ') ].join(' ');
})
.filter(Boolean)
.join(';');
return policyString;
}
module.exports = getCspPolicy;
......@@ -10,6 +10,8 @@ import poaSokolIcon from 'icons/networks/poa-sokol.svg';
import poaIcon from 'icons/networks/poa.svg';
import rskIcon from 'icons/networks/rsk.svg';
import parseNetworkConfig from './parseNetworkConfig';
// will change later when we agree how to host network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'xdai/mainnet': gnosisIcon,
......@@ -24,15 +26,13 @@ const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGEleme
'artis/sigma1': artisIcon,
};
export const NETWORKS: Array<Network> = (() => {
try {
const networksFromConfig: Array<Network> = JSON.parse(process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS || '[]');
return networksFromConfig.map((network) => ({ ...network, icon: network.icon || ICONS[`${ network.type }/${ network.subType }`] }));
} catch (error) {
return [];
}
const NETWORKS: Array<Network> = (() => {
const networksFromConfig: Array<Network> = parseNetworkConfig();
return networksFromConfig.map((network) => ({ ...network, icon: network.icon || ICONS[`${ network.type }/${ network.subType }`] }));
})();
export default NETWORKS;
// for easy env creation
// const FOR_CONFIG = [
// {
......@@ -105,13 +105,3 @@ export const NETWORKS: Array<Network> = (() => {
// group: 'other',
// },
// ];
export const ACCOUNT_ROUTES = [ '/watchlist', '/tag_address', '/tag_transaction', '/public_tags_request', '/api_key', '/custom_abi' ];
export function isAccountRoute(route: string) {
return ACCOUNT_ROUTES.includes(route);
}
export function getAvailablePaths() {
return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType } }));
}
import NETWORKS from './availableNetworks';
export default function getAvailablePaths() {
return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType } }));
}
export const ACCOUNT_ROUTES = [ '/watchlist', '/tag_address', '/tag_transaction', '/public_tags_request', '/api_key', '/custom_abi' ];
export default function isAccountRoute(route: string) {
return ACCOUNT_ROUTES.includes(route);
}
// should be CommonJS module since it used for next.config.js
function parseNetworkConfig() {
try {
return JSON.parse(process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS || '[]');
} catch (error) {
return [];
}
}
module.exports = parseNetworkConfig;
......@@ -2,6 +2,8 @@ const { withSentryConfig } = require('@sentry/nextjs');
const withReactSvg = require('next-react-svg');
const path = require('path');
const headers = require('./configs/nextjs/headers');
const moduleExports = {
include: path.resolve(__dirname, 'icons'),
reactStrictMode: true,
......@@ -17,6 +19,7 @@ const moduleExports = {
},
];
},
headers,
output: 'standalone',
};
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import ApiKeys from 'ui/pages/ApiKeys';
const ApiKeysPage: NextPage = () => {
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import CustomAbi from 'ui/pages/CustomAbi';
const CustomAbiPage: NextPage = () => {
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import PublicTags from 'ui/pages/PublicTags';
const PublicTagsPage: NextPage = () => {
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import PrivateTags from 'ui/pages/PrivateTags';
const AddressTagsPage: NextPage = () => {
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import PrivateTags from 'ui/pages/PrivateTags';
const TransactionTagsPage: NextPage = () => {
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import WatchList from 'ui/pages/Watchlist';
const WatchListPage: NextPage = () => {
......
......@@ -2,7 +2,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import Head from 'next/head';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import MyProfile from 'ui/pages/MyProfile';
const MyProfilePage: NextPage = () => {
......
......@@ -3,7 +3,7 @@ import type { NextPage, GetStaticPaths } from 'next';
import { useRouter } from 'next/router';
import React from 'react';
import { getAvailablePaths } from 'lib/networks';
import getAvailablePaths from 'lib/networks/getAvailablePaths';
import Page from 'ui/shared/Page/Page';
const Home: NextPage = () => {
......
......@@ -13,10 +13,6 @@ class MyDocument extends Document {
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,500;0,600;1,400&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<ColorModeScript initialColorMode={ theme.config.initialColorMode }/>
......
......@@ -4,7 +4,7 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import { NETWORKS } from 'lib/networks';
import NETWORKS from 'lib/networks/availableNetworks';
import NetworkMenuLink from './NetworkMenuLink';
......
......@@ -5,7 +5,7 @@ import React from 'react';
import type { NetworkGroup } from 'types/networks';
import { NETWORKS } from 'lib/networks';
import NETWORKS from 'lib/networks/availableNetworks';
import NetworkMenuLink from './NetworkMenuLink';
......
......@@ -6,7 +6,7 @@ import type { Network } from 'types/networks';
import checkIcon from 'icons/check.svg';
import placeholderIcon from 'icons/networks/placeholder.svg';
import { isAccountRoute } from 'lib/networks';
import isAccountRoute from 'lib/networks/isAccountRoute';
import useColors from './useColors';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment