From 37375b300a344a0634b655bebbb2fe94c8a5b19f Mon Sep 17 00:00:00 2001 From: Mohan Date: Thu, 25 Jul 2024 13:36:25 +0530 Subject: [PATCH] feat: security headers for apps (#65) * feat: Add next-middleware library and related files * feat: renames files for middleware * feat: remove unsafe-inline for style-src * chore: include object-src * chore: remove unsafe inline for firefox * chore: IMPORTANT - got A+ but changing the policy to make it work as it is broken now * chore: adding unsafe-inline for style-src * chore: remove font-src, object-src and scriptSrc to have unsafe-inline (for firefox) * refractor: move constants above the function in cspHeader, remove browerName usage as it works on firefox * chore: add 'nonce' support for style-src and commented unsafe-inline * chore: add nonce * chore: remove nonce and add unsafe-inline for style * chore: add api.thegraph.com origin * feat: add middleware to other apps * feat: remove headers from next.config.js for bond & tokenomics app * chore: rename getCspHeader function name * Update libs/common-middleware/src/lib/cspHeader.ts Co-authored-by: Josh Miller <31908788+truemiller@users.noreply.github.com> * refactor: Remove duplicate code for address prohibition check * refactor: Remove duplicate code for address prohibition check * feat: Add Vercel links to CSP allowed origins * feat: Add gateway links to CSP allowed origins * feat: Update IPFS gateway links in CSP allowed origins --------- Co-authored-by: Josh Miller <31908788+truemiller@users.noreply.github.com> --- .../List/IpfsHashGenerationModal/index.jsx | 18 ++- .../common-util/Login/LoginV2.jsx | 3 +- .../common-util/functions/index.jsx | 9 +- apps/autonolas-registry/middleware.ts | 152 +----------------- apps/bond/common-util/Login/LoginV2.jsx | 3 +- apps/bond/common-util/functions/addresses.js | 7 - apps/bond/common-util/functions/index.js | 1 - apps/bond/middleware.ts | 4 + apps/bond/next.config.js | 35 ---- .../govern/common-util/functions/addresses.ts | 8 - apps/govern/components/Login/LoginV2.tsx | 3 +- apps/govern/middleware.ts | 4 + apps/launch/middleware.ts | 4 + apps/tokenomics/common-util/Login/LoginV2.jsx | 3 +- .../common-util/functions/addresses.js | 7 - .../tokenomics/common-util/functions/index.js | 1 - apps/tokenomics/middleware.ts | 4 + apps/tokenomics/next.config.js | 25 --- libs/common-middleware/.eslintrc.json | 18 +++ libs/common-middleware/README.md | 5 + libs/common-middleware/project.json | 13 ++ libs/common-middleware/src/index.ts | 1 + libs/common-middleware/src/lib/cspHeader.ts | 143 ++++++++++++++++ .../src/lib/prohibitedCountries.ts | 13 ++ libs/common-middleware/src/middleware.ts | 39 +++++ libs/common-middleware/tsconfig.json | 17 ++ libs/common-middleware/tsconfig.lib.json | 25 +++ libs/util-prohibited-data/src/index.ts | 3 + tsconfig.base.json | 2 + 29 files changed, 323 insertions(+), 247 deletions(-) delete mode 100644 apps/bond/common-util/functions/addresses.js create mode 100644 apps/bond/middleware.ts create mode 100644 apps/govern/middleware.ts create mode 100644 apps/launch/middleware.ts delete mode 100644 apps/tokenomics/common-util/functions/addresses.js create mode 100644 apps/tokenomics/middleware.ts create mode 100644 libs/common-middleware/.eslintrc.json create mode 100644 libs/common-middleware/README.md create mode 100644 libs/common-middleware/project.json create mode 100644 libs/common-middleware/src/index.ts create mode 100644 libs/common-middleware/src/lib/cspHeader.ts create mode 100644 libs/common-middleware/src/lib/prohibitedCountries.ts create mode 100644 libs/common-middleware/src/middleware.ts create mode 100644 libs/common-middleware/tsconfig.json create mode 100644 libs/common-middleware/tsconfig.lib.json diff --git a/apps/autonolas-registry/common-util/List/IpfsHashGenerationModal/index.jsx b/apps/autonolas-registry/common-util/List/IpfsHashGenerationModal/index.jsx index 92824662..d1881bcb 100644 --- a/apps/autonolas-registry/common-util/List/IpfsHashGenerationModal/index.jsx +++ b/apps/autonolas-registry/common-util/List/IpfsHashGenerationModal/index.jsx @@ -1,4 +1,4 @@ -import { Button, Form, Input, Select } from 'antd'; +import { Button, Flex, Form, Input, Select } from 'antd'; import isNil from 'lodash/isNil'; import PropTypes from 'prop-types'; import React, { Fragment, useState } from 'react'; @@ -220,7 +220,21 @@ export const IpfsHashGenerationModal = ({ + + Represents your NFT on marketplaces such as OpenSea. Current supported domains are: + + + {/* TODO: fetch from middleware constant */} +
    +
  • https://gateway.autonolas.tech/ipfs/*
  • +
  • https://gateway.pinata.cloud/ipfs/*
  • +
  • https://*.arweave.net/
  • +
+
+ + } >
diff --git a/apps/autonolas-registry/common-util/Login/LoginV2.jsx b/apps/autonolas-registry/common-util/Login/LoginV2.jsx index 2cce86d4..db2af763 100644 --- a/apps/autonolas-registry/common-util/Login/LoginV2.jsx +++ b/apps/autonolas-registry/common-util/Login/LoginV2.jsx @@ -9,10 +9,11 @@ import { useAccount, useBalance, useDisconnect, useSwitchChain } from 'wagmi'; import { CannotConnectAddressOfacError, notifyError, useScreen } from '@autonolas/frontend-library'; +import { isAddressProhibited } from 'libs/util-prohibited-data/src/index'; + import { setUserBalance } from 'store/setup'; import { YellowButton } from '../YellowButton'; -import { isAddressProhibited } from '../functions'; import { useHelpers } from '../hooks'; import { SolanaWallet } from './SolanaWallet'; diff --git a/apps/autonolas-registry/common-util/functions/index.jsx b/apps/autonolas-registry/common-util/functions/index.jsx index 079cfcc3..dd28e049 100644 --- a/apps/autonolas-registry/common-util/functions/index.jsx +++ b/apps/autonolas-registry/common-util/functions/index.jsx @@ -1,5 +1,6 @@ import { PublicKey } from '@solana/web3.js'; import { ethers } from 'ethers'; +import { isString } from 'lodash'; import { getChainIdOrDefaultToMainnet as getChainIdOrDefaultToMainnetFn, @@ -10,9 +11,6 @@ import { sendTransaction as sendTransactionFn, } from '@autonolas/frontend-library'; -import prohibitedAddresses from 'libs/util-prohibited-data/src/lib/prohibited-addresses.json'; -import { isString, toLower } from 'lodash'; - import { VM_TYPE } from '../../util/constants'; import { RPC_URLS } from '../Contracts'; import { SUPPORTED_CHAINS } from '../Login'; @@ -178,11 +176,6 @@ export const checkIfGnosisSafe = async (account, provider) => { */ export const doesNetworkHaveValidServiceManagerTokenFn = (chainId) => !!chainId; -export const isAddressProhibited = (address) => { - const addresses = prohibitedAddresses.map((e) => toLower(e)); - return addresses.includes(toLower(address)); -}; - const doesPathIncludesComponents = (path) => !!path?.includes('components'); const doesPathIncludesAgents = (path) => !!path?.includes('agents'); export const doesPathIncludesServices = (path) => !!path?.includes('services'); diff --git a/apps/autonolas-registry/middleware.ts b/apps/autonolas-registry/middleware.ts index d76d6935..88188c61 100644 --- a/apps/autonolas-registry/middleware.ts +++ b/apps/autonolas-registry/middleware.ts @@ -1,150 +1,4 @@ -import nextSafe from 'next-safe'; -import { NextRequest, NextResponse, userAgent } from 'next/server'; +import { config, middleware } from 'libs/common-middleware/src'; -import prohibitedCountries from 'libs/util-prohibited-data/src/lib/prohibited-countries.json'; - -const prohibitedCountriesCode = Object.values(prohibitedCountries); - -const isDev = process.env.NODE_ENV !== 'production'; - -const getCspHeader = (browserName?: string) => { - if (!process.env.NEXT_PUBLIC_AUTONOLAS_SUB_GRAPH_URL) return []; - - const walletconnectSrc = ['https://verify.walletconnect.org', 'https://verify.walletconnect.com']; - - const connectSrc: CSPDirective = [ - "'self'", - ...walletconnectSrc, - 'https://*.olas.network/', - 'https://*.autonolas.tech/', - 'https://rpc.walletconnect.com/', - 'wss://relay.walletconnect.org/', - 'wss://relay.walletconnect.com/', - 'https://explorer-api.walletconnect.com/', - 'https://eth-mainnet.g.alchemy.com/v2/', - 'https://eth-goerli.g.alchemy.com/v2/', - 'https://gno.getblock.io/', - 'https://polygon-mainnet.g.alchemy.com/v2/', - 'https://polygon-mumbai-bor.publicnode.com/', - 'https://rpc.chiado.gnosis.gateway.fm/', - 'https://safe-transaction-mainnet.safe.global/api/v1/', - 'https://safe-transaction-goerli.safe.global/api/', - 'https://safe-transaction-gnosis-chain.safe.global/api/', - 'https://safe-transaction-polygon.safe.global/api/', - 'https://vercel.live/', - 'https://api.devnet.solana.com/', - 'wss://api.devnet.solana.com/', - 'https://api.mainnet-beta.solana.com/', - 'wss://api.mainnet-beta.solana.com/', - 'https://holy-convincing-bird.solana-mainnet.quiknode.pro/', - 'wss://holy-convincing-bird.solana-mainnet.quiknode.pro/', - 'https://arb1.arbitrum.io/rpc/', - 'https://sepolia-rollup.arbitrum.io/rpc', - 'https://rpc.gnosischain.com/', - 'https://mainnet.base.org/', - 'https://sepolia.base.org/', - 'https://mainnet.optimism.io', - 'https://sepolia.optimism.io/', - 'https://forno.celo.org', - 'https://alfajores-forno.celo-testnet.org', - 'https://api.web3modal.com/', - 'wss://www.walletlink.org/rpc', - 'wss://*.pusher.com/', - process.env.NEXT_PUBLIC_AUTONOLAS_SUB_GRAPH_URL, - ]; - - if (isDev) { - connectSrc.push('http://localhost'); - connectSrc.push('ws://localhost'); - } - - const scriptSrc = ["'self'", 'https://vercel.live/', 'https://fonts.googleapis.com/']; - - // Firefox blocks inline scripts by default and it's an issue with Metamask - // reference: https://github.com/MetaMask/metamask-extension/issues/3133 - if (browserName === 'Firefox') { - scriptSrc.push("'unsafe-inline'"); - } - - const nextSafeHeaders = - typeof nextSafe === 'function' - ? // TODO - // @ts-expect-error: For some reason, TypeScript is not recognizing the function - nextSafe({ - isDev, - /** - * Content Security Policy - * @see https://content-security-policy.com/ - */ - contentSecurityPolicy: { - 'default-src': "'none'", - 'script-src': scriptSrc, - 'connect-src': connectSrc, - 'img-src': [ - "'self'", - 'blob:', - 'data:', - 'https://*.autonolas.tech/', - 'https://explorer-api.walletconnect.com/w3m/', - ...walletconnectSrc, - ], - 'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com/'], - 'frame-src': ["'self'", 'https://vercel.live/', ...walletconnectSrc], - }, - permissionsPolicyDirectiveSupport: ['standard'], - }) - : []; - - const headers = [ - ...nextSafeHeaders, - { - key: 'Strict-Transport-Security', - value: 'max-age=31536000; includeSubDomains', - }, - ]; - - return headers; -}; - -const getRedirectUrl = (pathName: string, countryName?: string) => { - const isProhibited = countryName ? prohibitedCountriesCode.includes(countryName) : false; - - if (pathName === '/not-legal') { - return isProhibited ? null : '/'; - } - return isProhibited ? '/not-legal' : null; -}; - -export default async function middleware(request: NextRequest) { - const country = request.geo?.country; - const redirectUrl = getRedirectUrl(request.nextUrl.pathname, country); - - const response = redirectUrl - ? NextResponse.redirect(new URL(redirectUrl, request.nextUrl)) - : NextResponse.next(); - - const browserName = userAgent(request)?.browser.name; - const cspHeaders = getCspHeader(browserName); - - // apply CSP headers - // https://nextjs.org/docs/app/building-your-application/routing/middleware#setting-headers - cspHeaders.forEach((header) => { - const { key, value } = header; - response.headers.set(key, value); - }); - - return response; -} - -export const config = { - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - api (API routes) - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - */ - '/((?!api|_next/static|_next/image|favicon.ico).*)', - ], -}; +export default middleware; +export { config }; diff --git a/apps/bond/common-util/Login/LoginV2.jsx b/apps/bond/common-util/Login/LoginV2.jsx index ed7132cb..8df0cacc 100644 --- a/apps/bond/common-util/Login/LoginV2.jsx +++ b/apps/bond/common-util/Login/LoginV2.jsx @@ -12,7 +12,8 @@ import { notifyError, } from '@autonolas/frontend-library'; -import { isAddressProhibited } from 'common-util/functions/addresses'; +import { isAddressProhibited } from 'libs/util-prohibited-data/src/index'; + import { getChainId, getChainIdOrDefaultToMainnet } from 'common-util/functions/frontend-library'; import { setChainId, setUserBalance } from 'store/setup'; diff --git a/apps/bond/common-util/functions/addresses.js b/apps/bond/common-util/functions/addresses.js deleted file mode 100644 index 795639ec..00000000 --- a/apps/bond/common-util/functions/addresses.js +++ /dev/null @@ -1,7 +0,0 @@ -import toLower from 'lodash/toLower'; -import prohibitedAddresses from 'libs/util-prohibited-data/src/lib/prohibited-addresses.json'; - -export const isAddressProhibited = (address) => { - const addresses = prohibitedAddresses.map((e) => toLower(e)); - return addresses.includes(toLower(address)); -}; diff --git a/apps/bond/common-util/functions/index.js b/apps/bond/common-util/functions/index.js index e3520124..19554e5e 100644 --- a/apps/bond/common-util/functions/index.js +++ b/apps/bond/common-util/functions/index.js @@ -1,4 +1,3 @@ -export * from './addresses'; export * from './chains'; export * from './errors'; export * from './ethers'; diff --git a/apps/bond/middleware.ts b/apps/bond/middleware.ts new file mode 100644 index 00000000..88188c61 --- /dev/null +++ b/apps/bond/middleware.ts @@ -0,0 +1,4 @@ +import { config, middleware } from 'libs/common-middleware/src'; + +export default middleware; +export { config }; diff --git a/apps/bond/next.config.js b/apps/bond/next.config.js index 21180c94..36638391 100644 --- a/apps/bond/next.config.js +++ b/apps/bond/next.config.js @@ -24,41 +24,6 @@ const nextConfig = { }; return config; }, - async headers() { - return [ - { - source: '/:path*', - headers: [ - { - key: 'Content-Security-Policy', - value: "frame-ancestors 'none';", - }, - { - key: 'X-Content-Type-Options', - value: 'nosniff', - }, - { - key: 'Referrer-Policy', - value: 'strict-origin-when-cross-origin', - }, - { - key: 'Strict-Transport-Security', - value: 'max-age=31536000; includeSubDomains', - }, - ], - }, - { - source: '/:all*(svg|jpg|jpeg|png|gif|ico|css|js|mov|mp4)', - headers: [ - { - key: 'Cache-Control', - value: 'public, max-age=31536000, must-revalidate', - }, - ], - }, - ]; - }, - }; const plugins = [ diff --git a/apps/govern/common-util/functions/addresses.ts b/apps/govern/common-util/functions/addresses.ts index 0e423c83..11057631 100644 --- a/apps/govern/common-util/functions/addresses.ts +++ b/apps/govern/common-util/functions/addresses.ts @@ -1,14 +1,6 @@ import { ethers } from 'ethers'; -import { toLower } from 'lodash'; import { Address } from 'viem'; -import prohibitedAddresses from 'libs/util-prohibited-data/src/lib/prohibited-addresses.json'; - -export const isAddressProhibited = (address: Address | undefined) => { - const addresses = prohibitedAddresses.map((e) => toLower(e)); - return addresses.includes(toLower(address)); -}; - export const getAddressFromBytes32 = (address: Address | string) => { return ('0x' + address.slice(-40)) as Address; }; diff --git a/apps/govern/components/Login/LoginV2.tsx b/apps/govern/components/Login/LoginV2.tsx index 3c742778..03fe134a 100644 --- a/apps/govern/components/Login/LoginV2.tsx +++ b/apps/govern/components/Login/LoginV2.tsx @@ -3,8 +3,9 @@ import { useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { useAccountEffect, useConfig, useDisconnect } from 'wagmi'; +import { isAddressProhibited } from 'libs/util-prohibited-data/src/index'; + import { INVALIDATE_AFTER_ACCOUNT_CHANGE } from 'common-util/constants/scopeKeys'; -import { isAddressProhibited } from 'common-util/functions'; import { queryClient } from 'context/Web3ModalProvider'; import { clearUserState } from 'store/govern'; import { useAppDispatch } from 'store/index'; diff --git a/apps/govern/middleware.ts b/apps/govern/middleware.ts new file mode 100644 index 00000000..88188c61 --- /dev/null +++ b/apps/govern/middleware.ts @@ -0,0 +1,4 @@ +import { config, middleware } from 'libs/common-middleware/src'; + +export default middleware; +export { config }; diff --git a/apps/launch/middleware.ts b/apps/launch/middleware.ts new file mode 100644 index 00000000..88188c61 --- /dev/null +++ b/apps/launch/middleware.ts @@ -0,0 +1,4 @@ +import { config, middleware } from 'libs/common-middleware/src'; + +export default middleware; +export { config }; diff --git a/apps/tokenomics/common-util/Login/LoginV2.jsx b/apps/tokenomics/common-util/Login/LoginV2.jsx index ed7132cb..8df0cacc 100644 --- a/apps/tokenomics/common-util/Login/LoginV2.jsx +++ b/apps/tokenomics/common-util/Login/LoginV2.jsx @@ -12,7 +12,8 @@ import { notifyError, } from '@autonolas/frontend-library'; -import { isAddressProhibited } from 'common-util/functions/addresses'; +import { isAddressProhibited } from 'libs/util-prohibited-data/src/index'; + import { getChainId, getChainIdOrDefaultToMainnet } from 'common-util/functions/frontend-library'; import { setChainId, setUserBalance } from 'store/setup'; diff --git a/apps/tokenomics/common-util/functions/addresses.js b/apps/tokenomics/common-util/functions/addresses.js deleted file mode 100644 index 396a40fa..00000000 --- a/apps/tokenomics/common-util/functions/addresses.js +++ /dev/null @@ -1,7 +0,0 @@ -import prohibitedAddresses from 'libs/util-prohibited-data/src/lib/prohibited-addresses.json'; -import toLower from 'lodash/toLower'; - -export const isAddressProhibited = (address) => { - const addresses = prohibitedAddresses.map((e) => toLower(e)); - return addresses.includes(toLower(address)); -}; diff --git a/apps/tokenomics/common-util/functions/index.js b/apps/tokenomics/common-util/functions/index.js index 4ae847cb..964ec0ad 100644 --- a/apps/tokenomics/common-util/functions/index.js +++ b/apps/tokenomics/common-util/functions/index.js @@ -1,4 +1,3 @@ -export * from './addresses'; export * from './errors'; export * from './ethers'; export * from './time'; diff --git a/apps/tokenomics/middleware.ts b/apps/tokenomics/middleware.ts new file mode 100644 index 00000000..88188c61 --- /dev/null +++ b/apps/tokenomics/middleware.ts @@ -0,0 +1,4 @@ +import { config, middleware } from 'libs/common-middleware/src'; + +export default middleware; +export { config }; diff --git a/apps/tokenomics/next.config.js b/apps/tokenomics/next.config.js index 1ab955c0..f1829759 100644 --- a/apps/tokenomics/next.config.js +++ b/apps/tokenomics/next.config.js @@ -40,31 +40,6 @@ const nextConfig = { }, ]; }, - async headers() { - return [ - { - source: '/:path*', - headers: [ - { - key: 'Content-Security-Policy', - value: "frame-ancestors 'none';", - }, - { - key: 'X-Content-Type-Options', - value: 'nosniff', - }, - { - key: 'Referrer-Policy', - value: 'strict-origin-when-cross-origin', - }, - { - key: 'Strict-Transport-Security', - value: 'max-age=31536000; includeSubDomains', - }, - ], - }, - ]; - }, }; const plugins = [ // Add more Next.js plugins to this list if needed. diff --git a/libs/common-middleware/.eslintrc.json b/libs/common-middleware/.eslintrc.json new file mode 100644 index 00000000..a39ac5d0 --- /dev/null +++ b/libs/common-middleware/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/common-middleware/README.md b/libs/common-middleware/README.md new file mode 100644 index 00000000..69743456 --- /dev/null +++ b/libs/common-middleware/README.md @@ -0,0 +1,5 @@ +# common-middleware + +Common middleware for apps, including: +- Prohibition of specific countries and addresses +- Implementation of security headers diff --git a/libs/common-middleware/project.json b/libs/common-middleware/project.json new file mode 100644 index 00000000..d81cb0af --- /dev/null +++ b/libs/common-middleware/project.json @@ -0,0 +1,13 @@ +{ + "name": "common-middleware", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/common-middleware/src", + "projectType": "library", + "tags": [], + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/libs/common-middleware/src/index.ts b/libs/common-middleware/src/index.ts new file mode 100644 index 00000000..965a85bc --- /dev/null +++ b/libs/common-middleware/src/index.ts @@ -0,0 +1 @@ +export { config, middleware } from './middleware'; diff --git a/libs/common-middleware/src/lib/cspHeader.ts b/libs/common-middleware/src/lib/cspHeader.ts new file mode 100644 index 00000000..44b2a302 --- /dev/null +++ b/libs/common-middleware/src/lib/cspHeader.ts @@ -0,0 +1,143 @@ +import nextSafe from 'next-safe'; + +const isDev = process.env.NODE_ENV !== 'production'; + +const WALLET_CONNECT_LINKS = [ + 'https://verify.walletconnect.org', + 'https://verify.walletconnect.com', +]; + +const VERCEL_LINKS = ['https://vercel.com', 'https://vercel.live/']; + +const GATEWAY_LINKS = [ + 'https://gateway.autonolas.tech/ipfs/*', + 'https://gateway.pinata.cloud/ipfs/*', + 'https://*.arweave.net/', + 'https://i.seadn.io/s/raw/files/', + 'https://www.askjimmy.xyz/images/', +]; + +const ALLOWED_ORIGINS = [ + // internal + "'self'", + 'https://*.olas.network/', + 'https://*.autonolas.tech/', + + // web3modal and wallet connect + ...WALLET_CONNECT_LINKS, + 'https://rpc.walletconnect.com/', + 'wss://relay.walletconnect.org/', + 'wss://relay.walletconnect.com/', + 'https://explorer-api.walletconnect.com/', + 'wss://*.pusher.com/', + 'wss://www.walletlink.org/rpc', + + // gnosis safe + 'https://safe-transaction-mainnet.safe.global/api/v1/', + 'https://safe-transaction-goerli.safe.global/api/', + 'https://safe-transaction-gnosis-chain.safe.global/api/', + 'https://safe-transaction-polygon.safe.global/api/', + + // chains + 'https://eth-mainnet.g.alchemy.com/v2/', + 'https://eth-goerli.g.alchemy.com/v2/', + 'https://gno.getblock.io/', + 'https://polygon-mainnet.g.alchemy.com/v2/', + 'https://polygon-mumbai-bor.publicnode.com/', + 'https://rpc.chiado.gnosis.gateway.fm/', + 'https://api.devnet.solana.com/', + 'wss://api.devnet.solana.com/', + 'https://api.mainnet-beta.solana.com/', + 'wss://api.mainnet-beta.solana.com/', + 'https://holy-convincing-bird.solana-mainnet.quiknode.pro/', + 'wss://holy-convincing-bird.solana-mainnet.quiknode.pro/', + 'https://arb1.arbitrum.io/rpc/', + 'https://sepolia-rollup.arbitrum.io/rpc', + 'https://rpc.gnosischain.com/', + 'https://mainnet.base.org/', + 'https://sepolia.base.org/', + 'https://mainnet.optimism.io', + 'https://sepolia.optimism.io/', + 'https://forno.celo.org', + 'https://alfajores-forno.celo-testnet.org', + 'https://api.web3modal.com/', + + // tenderly + 'https://virtual.mainnet.rpc.tenderly.co/', + + // others + 'https://api.thegraph.com/', + 'https://sockjs-us3.pusher.com/', + + ...VERCEL_LINKS, +]; + +const SCRIPT_SRC = ["'self'", 'https://vercel.live/', 'https://fonts.googleapis.com/']; + +export const getCspHeaders = () => { + if (!process.env.NEXT_PUBLIC_AUTONOLAS_SUB_GRAPH_URL) return []; + + const connectSrc: CSPDirective = [ + ...ALLOWED_ORIGINS, + + // env variables + process.env.NEXT_PUBLIC_AUTONOLAS_SUB_GRAPH_URL, + ]; + + if (isDev) { + connectSrc.push('http://localhost'); + connectSrc.push('ws://localhost'); + } + + const getNextSafeHeaders = () => { + if (typeof nextSafe !== 'function') return []; + + // @ts-expect-error: For some reason, TypeScript is not recognizing the function + return nextSafe({ + isDev, + /** + * Content Security Policy + * @see https://content-security-policy.com/ + */ + contentSecurityPolicy: { + 'default-src': "'none'", + 'script-src': SCRIPT_SRC, + 'connect-src': connectSrc, + 'img-src': [ + "'self'", + 'blob:', + 'data:', + 'https://*.autonolas.tech/', + 'https://explorer-api.walletconnect.com/w3m/', + ...WALLET_CONNECT_LINKS, + ...GATEWAY_LINKS, + ...VERCEL_LINKS, + ], + /** + * It is less harmful to allow 'unsafe-inline' in style-src, please read the article below + * @see https://scotthelme.co.uk/can-you-get-pwned-with-css/ + */ + 'style-src': [ + "'self'", + 'https://fonts.googleapis.com/', + "'unsafe-inline'", + 'https://vercel.live/fonts', + ], + 'font-src': ["'self'", 'https://fonts.gstatic.com'], + 'frame-src': ["'self'", 'https://vercel.live/', ...WALLET_CONNECT_LINKS], + }, + permissionsPolicyDirectiveSupport: ['standard'], + }); + }; + + /** + * Some headers might throw warnings in the console - they are safe to ignore. + * @see https://trezy.gitbook.io/next-safe/usage/troubleshooting#why-do-i-see-so-many-unrecognized-feature-warnings + */ + const headers = [ + ...getNextSafeHeaders(), + { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }, + ]; + + return headers; +}; diff --git a/libs/common-middleware/src/lib/prohibitedCountries.ts b/libs/common-middleware/src/lib/prohibitedCountries.ts new file mode 100644 index 00000000..b992b378 --- /dev/null +++ b/libs/common-middleware/src/lib/prohibitedCountries.ts @@ -0,0 +1,13 @@ +/* eslint-disable @nx/enforce-module-boundaries */ +import prohibitedAddresses from 'libs/util-prohibited-data/src/lib/prohibited-addresses.json'; + +export const getRedirectUrl = async (pathName: string, countryName?: string) => { + const prohibitedCountriesCode = Object.values(prohibitedAddresses) as unknown as string[]; + + const isProhibited = countryName ? prohibitedCountriesCode.includes(countryName) : false; + + if (pathName === '/not-legal') { + return isProhibited ? null : '/'; + } + return isProhibited ? '/not-legal' : null; +}; diff --git a/libs/common-middleware/src/middleware.ts b/libs/common-middleware/src/middleware.ts new file mode 100644 index 00000000..2e7d94b0 --- /dev/null +++ b/libs/common-middleware/src/middleware.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { getCspHeaders } from './lib/cspHeader'; +import { getRedirectUrl } from './lib/prohibitedCountries'; + +export const middleware = async (request: NextRequest) => { + const country = request.geo?.country; + const redirectUrl = await getRedirectUrl(request.nextUrl.pathname, country); + + const response = redirectUrl + ? NextResponse.redirect(new URL(redirectUrl, request.nextUrl)) + : NextResponse.next(); + + const cspHeaders = getCspHeaders(); + + /** + * apply CSP headers + * @see https://nextjs.org/docs/app/building-your-application/routing/middleware#setting-headers + */ + cspHeaders.forEach((header) => { + const { key, value } = header; + response.headers.set(key, value); + }); + + return response; +}; + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + ], +}; diff --git a/libs/common-middleware/tsconfig.json b/libs/common-middleware/tsconfig.json new file mode 100644 index 00000000..95cfeb24 --- /dev/null +++ b/libs/common-middleware/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/common-middleware/tsconfig.lib.json b/libs/common-middleware/tsconfig.lib.json new file mode 100644 index 00000000..08e579bc --- /dev/null +++ b/libs/common-middleware/tsconfig.lib.json @@ -0,0 +1,25 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "next", + "@nx/next/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/util-prohibited-data/src/index.ts b/libs/util-prohibited-data/src/index.ts index fe50d205..5c7f8481 100644 --- a/libs/util-prohibited-data/src/index.ts +++ b/libs/util-prohibited-data/src/index.ts @@ -2,8 +2,11 @@ import { toLower } from 'lodash'; import { Address } from 'viem'; import prohibitedAddresses from './lib/prohibited-addresses.json'; +import prohibitedCountries from './lib/prohibited-countries.json'; export const isAddressProhibited = (address: Address | undefined) => { const addresses = prohibitedAddresses.map((e) => toLower(e)); return addresses.includes(toLower(address)); }; + +export { prohibitedCountries, prohibitedAddresses }; diff --git a/tsconfig.base.json b/tsconfig.base.json index b33be9b6..1cad8f3d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -33,6 +33,8 @@ "@autonolas-frontend-mono/feature-service-status-info": [ "libs/feature-service-status-info/src/index.ts" ], + "@autonolas-frontend-mono/common-middleware": ["libs/common-middleware/src/index.ts"], + "@autonolas-frontend-mono/common-middleware/server": ["libs/common-middleware/src/server.ts"], "@autonolas-frontend-mono/service-status-info": ["libs/service-status-info/src/index.ts"], "@autonolas-frontend-mono/ui-theme": ["libs/ui-theme/src/index.ts"], "@autonolas-frontend-mono/util-contracts": ["libs/util-contracts/src/index.ts"],