From f25239815ed447f9a551337aa4db0f4eabc4b09e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Sun, 15 Dec 2024 18:57:58 +0100 Subject: [PATCH 01/79] fix: xss dom security issue --- src/shared/utils/url.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js index bd2599d36..25e7ba6a0 100644 --- a/src/shared/utils/url.js +++ b/src/shared/utils/url.js @@ -14,7 +14,13 @@ import { BUCKETS } from 'utils/challenge-listing/buckets'; */ export function getCurrentUrl() { if (isomorphy.isServerSide()) return null; - return window.location.href; + const url = window.location.href; + + if (typeof url === 'string' && url.startsWith('http')) { + return url; + } + + return null; } /** From 8e86304defa64181d1d2d6c9da4d8f63dcc1d24e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 16 Dec 2024 16:41:55 +0100 Subject: [PATCH 02/79] fix: lint --- src/shared/utils/url.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js index 25e7ba6a0..6245ff277 100644 --- a/src/shared/utils/url.js +++ b/src/shared/utils/url.js @@ -15,7 +15,7 @@ import { BUCKETS } from 'utils/challenge-listing/buckets'; export function getCurrentUrl() { if (isomorphy.isServerSide()) return null; const url = window.location.href; - + if (typeof url === 'string' && url.startsWith('http')) { return url; } From 7777ccf91a543ee158c6eb4f877698fe61ba9362 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 16 Dec 2024 22:43:41 +0100 Subject: [PATCH 03/79] fix: default user privilege --- Dockerfile | 4 ++++ automated-smoke-test/Dockerfile | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7691feebd..123609baf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,14 @@ FROM node:8.11.2 LABEL app="Community App" version="1.0" +RUN useradd -m -s /bin/bash appuser WORKDIR /opt/app COPY . . +RUN chown -R appuser:appuser /opt/app +USER appuser + ################################################################################ # Receiving of build arguments. diff --git a/automated-smoke-test/Dockerfile b/automated-smoke-test/Dockerfile index b228769e6..a5ad40ec6 100644 --- a/automated-smoke-test/Dockerfile +++ b/automated-smoke-test/Dockerfile @@ -1,4 +1,5 @@ FROM node:10.17.0-stretch +RUN useradd -m -s /bin/bash appuser RUN apt update RUN apt install sudo RUN sudo apt-get update; sudo apt-get install -y openjdk-8-jre openjdk-8-jre-headless openjdk-8-jdk openjdk-8-jdk-headless; @@ -26,6 +27,8 @@ RUN printf '#!/bin/sh\nXvfb :99 -screen 0 1280x1024x24 &\nexec "$@"\n' > /tmp/en COPY . /automated-smoke-test WORKDIR /automated-smoke-test +RUN chown -R appuser:appuser /automated-smoke-test +USER appuser RUN npm install RUN ./node_modules/.bin/webdriver-manager update --versions.chrome=="$(google-chrome -version)" ENTRYPOINT ["/docker-entrypoint.sh"] From c38c3ba1b8cb4ffd43b3df888ac4344919a51823 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 17 Dec 2024 22:09:08 +0100 Subject: [PATCH 04/79] fix: denial of service possibilities --- .circleci/config.yml | 1 + src/server/index.js | 41 +++++++++++++++++++-- src/server/services/communities.js | 59 ++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 621db6412..6383e3c7c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -360,6 +360,7 @@ workflows: - develop - TOP-1390 - PM-191-2 + - pm-199 # This is alternate dev env for parallel testing # Deprecate this workflow due to beta env shutdown # https://topcoder.atlassian.net/browse/CORE-251 diff --git a/src/server/index.js b/src/server/index.js index 393a72b9a..53e2398eb 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -42,9 +42,44 @@ global.atob = atob; const CMS_BASE_URL = `https://app.contentful.com/spaces/${config.SECRET.CONTENTFUL.SPACE_ID}`; -let ts = path.resolve(__dirname, '../../.build-info'); -ts = JSON.parse(fs.readFileSync(ts)); -ts = moment(ts.timestamp).valueOf(); +const getTimestamp = async () => { + try { + // Step 1: Resolve and validate file path + const filePath = path.resolve(__dirname, '../../.build-info'); + if (!filePath.startsWith(path.resolve(__dirname))) { + throw new Error('Invalid file path detected'); + } + + // Step 2: Read file asynchronously and add file size limit check + const MAX_FILE_SIZE = 10 * 1024; // 10 KB max file size + const stats = await fs.stat(filePath); + if (stats.size > MAX_FILE_SIZE) { + throw new Error('File is too large and may cause DoS issues'); + } + + const fileContent = await fs.readFile(filePath, 'utf-8'); + + // Step 3: Validate and parse JSON safely + let tsData; + try { + tsData = JSON.parse(fileContent); + } catch (parseErr) { + throw new Error('Invalid JSON format in file'); + } + + // Step 4: Validate timestamp format + if (!tsData || !tsData.timestamp) { + throw new Error('Timestamp is missing in the JSON file'); + } + + // Step 5: Process timestamp + return moment(tsData.timestamp).valueOf(); + } catch (err) { + console.error('Error:', err.message); + } +}; + +const ts = await getTimestamp(); const sw = `sw.js${process.env.NODE_ENV === 'production' ? '' : '?debug'}`; const swScope = '/challenges'; // we are currently only interested in improving challenges pages diff --git a/src/server/services/communities.js b/src/server/services/communities.js index 90a02b68f..12b411280 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -31,23 +31,50 @@ async function getGroupsService() { } const METADATA_PATH = path.resolve(__dirname, '../tc-communities'); -const VALID_IDS = isomorphy.isServerSide() -&& fs.readdirSync(METADATA_PATH).filter((id) => { - /* Here we check which ids are correct, and also popuate SUBDOMAIN_COMMUNITY - * map. */ - const uri = path.resolve(METADATA_PATH, id, 'metadata.json'); - try { - const meta = JSON.parse(fs.readFileSync(uri, 'utf8')); - if (meta.subdomains) { - meta.subdomains.forEach((subdomain) => { - SUBDOMAIN_COMMUNITY[subdomain] = id; - }); + +const VALID_IDS = isomorphy.isServerSide() && + fs.readdirSync(METADATA_PATH).filter((id) => { + /* Validate and sanitize the ID */ + if (!/^[a-zA-Z0-9_-]+$/.test(id)) { + console.warn(`Skipping invalid ID: ${id}`); + return false; } - return true; - } catch (e) { - return false; - } -}); + + const uri = path.resolve(METADATA_PATH, id, 'metadata.json'); + try { + // Check if the file exists and is not too large + if (!fs.existsSync(uri)) { + console.warn(`Metadata file does not exist for ID: ${id}`); + return false; + } + + const stats = fs.statSync(uri); + const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1 MB + if (stats.size > MAX_FILE_SIZE) { + console.warn(`Metadata file too large for ID: ${id}`); + return false; + } + + // Parse and validate JSON + const meta = JSON.parse(fs.readFileSync(uri, 'utf8')); + + // Check if "subdomains" is a valid array + if (Array.isArray(meta.subdomains)) { + meta.subdomains.forEach((subdomain) => { + if (typeof subdomain === 'string') { + SUBDOMAIN_COMMUNITY[subdomain] = id; + } else { + console.warn(`Invalid subdomain entry for ID: ${id}`); + } + }); + } + + return true; // ID is valid if all checks pass + } catch (e) { + console.error(`Error processing metadata for ID: ${id}`, e.message); + return false; + } + }); /** * Given an array of group IDs, returns an array containing IDs of all those From de5c522d4284a261bb544f30852721b014ecd51d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 12:52:49 +0100 Subject: [PATCH 05/79] fix: lint --- src/server/index.js | 92 +++++++++++++++--------------- src/server/services/communities.js | 4 +- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index 53e2398eb..944b4c7a6 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -79,57 +79,57 @@ const getTimestamp = async () => { } }; -const ts = await getTimestamp(); - const sw = `sw.js${process.env.NODE_ENV === 'production' ? '' : '?debug'}`; const swScope = '/challenges'; // we are currently only interested in improving challenges pages const tcoPattern = new RegExp(/^tco\d{2}\.topcoder(?:-dev)?\.com$/i); const universalNavUrl = config.UNIVERSAL_NAV_URL; -const EXTRA_SCRIPTS = [ - ``, - ``, - ``, - ``, - ` - - `, -]; + }).catch((err)=>{console.log('SW registration failed: ',err)}) + } + `, + ``, + ``, + ``, + ` + + `, + ] +} const MODE = process.env.BABEL_ENV; @@ -147,9 +147,11 @@ async function beforeRender(req, suggestedConfig) { await DoSSR(req, store, Application); +const ts = await getTimestamp(); + return { configToInject: { ...suggestedConfig, EXCHANGE_RATES: rates }, - extraScripts: EXTRA_SCRIPTS, + extraScripts: getExtraScripts(ts), store, }; } diff --git a/src/server/services/communities.js b/src/server/services/communities.js index 12b411280..06607b2f5 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -32,8 +32,8 @@ async function getGroupsService() { const METADATA_PATH = path.resolve(__dirname, '../tc-communities'); -const VALID_IDS = isomorphy.isServerSide() && - fs.readdirSync(METADATA_PATH).filter((id) => { +const VALID_IDS = isomorphy.isServerSide() + && fs.readdirSync(METADATA_PATH).filter((id) => { /* Validate and sanitize the ID */ if (!/^[a-zA-Z0-9_-]+$/.test(id)) { console.warn(`Skipping invalid ID: ${id}`); From 1bc7b9574fd3e04107a24a4420984274b8e81b3d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 12:54:13 +0100 Subject: [PATCH 06/79] fix: lint --- src/server/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index 944b4c7a6..5246b7303 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -44,13 +44,11 @@ const CMS_BASE_URL = `https://app.contentful.com/spaces/${config.SECRET.CONTENTF const getTimestamp = async () => { try { - // Step 1: Resolve and validate file path const filePath = path.resolve(__dirname, '../../.build-info'); if (!filePath.startsWith(path.resolve(__dirname))) { throw new Error('Invalid file path detected'); } - // Step 2: Read file asynchronously and add file size limit check const MAX_FILE_SIZE = 10 * 1024; // 10 KB max file size const stats = await fs.stat(filePath); if (stats.size > MAX_FILE_SIZE) { @@ -59,7 +57,6 @@ const getTimestamp = async () => { const fileContent = await fs.readFile(filePath, 'utf-8'); - // Step 3: Validate and parse JSON safely let tsData; try { tsData = JSON.parse(fileContent); @@ -67,12 +64,10 @@ const getTimestamp = async () => { throw new Error('Invalid JSON format in file'); } - // Step 4: Validate timestamp format if (!tsData || !tsData.timestamp) { throw new Error('Timestamp is missing in the JSON file'); } - // Step 5: Process timestamp return moment(tsData.timestamp).valueOf(); } catch (err) { console.error('Error:', err.message); From 7e9b12041f2407086099ccb4b6fa29e7c16f9055 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 13:12:12 +0100 Subject: [PATCH 07/79] fix: get valid ids in communities js --- src/server/services/communities.js | 85 +++++++++++++++++------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/server/services/communities.js b/src/server/services/communities.js index 06607b2f5..d98099947 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -32,49 +32,57 @@ async function getGroupsService() { const METADATA_PATH = path.resolve(__dirname, '../tc-communities'); -const VALID_IDS = isomorphy.isServerSide() - && fs.readdirSync(METADATA_PATH).filter((id) => { - /* Validate and sanitize the ID */ - if (!/^[a-zA-Z0-9_-]+$/.test(id)) { - console.warn(`Skipping invalid ID: ${id}`); - return false; - } - const uri = path.resolve(METADATA_PATH, id, 'metadata.json'); - try { - // Check if the file exists and is not too large - if (!fs.existsSync(uri)) { - console.warn(`Metadata file does not exist for ID: ${id}`); - return false; - } - const stats = fs.statSync(uri); - const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1 MB - if (stats.size > MAX_FILE_SIZE) { - console.warn(`Metadata file too large for ID: ${id}`); - return false; - } +const getValidIds = async (METADATA_PATH) => { + if (!isomorphy.isServerSide()) return []; - // Parse and validate JSON - const meta = JSON.parse(fs.readFileSync(uri, 'utf8')); + const promise = new Promise(); + const VALID_IDS = []; - // Check if "subdomains" is a valid array - if (Array.isArray(meta.subdomains)) { - meta.subdomains.forEach((subdomain) => { - if (typeof subdomain === 'string') { - SUBDOMAIN_COMMUNITY[subdomain] = id; - } else { - console.warn(`Invalid subdomain entry for ID: ${id}`); + try { + await fs.readdir(METADATA_PATH, async (err, ids) => { + for (const id of ids) { + const uri = path.resolve(METADATA_PATH, id, 'metadata.json'); + + try { + await fs.access(uri); + + const stats = await fs.stat(uri); + const MAX_FILE_SIZE = 1 * 1024 * 1024; + if (stats.size > MAX_FILE_SIZE) { + console.warn(`Metadata file too large for ID: ${id}`); + continue; } - }); + + const meta = JSON.parse(await fs.readFile(uri, 'utf8')); + + if (Array.isArray(meta.subdomains)) { + meta.subdomains.forEach((subdomain) => { + if (typeof subdomain === 'string') { + SUBDOMAIN_COMMUNITY[subdomain] = id; + } else { + console.warn(`Invalid subdomain entry for ID: ${id}`); + } + }); + } + + VALID_IDS.push(id); + } catch (e) { + console.error(`Error processing metadata for ID: ${id}`, e.message); + promise.reject(`Error processing metadata for ID: ${id}`) + } } - return true; // ID is valid if all checks pass - } catch (e) { - console.error(`Error processing metadata for ID: ${id}`, e.message); - return false; - } - }); + promise.resolve(VALID_IDS); + }); + + return VALID_IDS; + } catch (err) { + console.error(`Error reading metadata directory: ${METADATA_PATH}`, err.message); + return []; + } +}; /** * Given an array of group IDs, returns an array containing IDs of all those @@ -167,10 +175,11 @@ getMetadata.maxage = 5 * 60 * 1000; // 5 min in ms. * @return {Promise} Resolves to the array of community data objects. Each of * the objects indludes only the most important data on the community. */ -export function getList(userGroupIds) { +export async function getList(userGroupIds) { const list = []; + const validIds = await getValidIds(METADATA_PATH); return Promise.all( - VALID_IDS.map(id => getMetadata(id).then((data) => { + validIds.map(id => getMetadata(id).then((data) => { if (!data.authorizedGroupIds || _.intersection(data.authorizedGroupIds, userGroupIds).length) { list.push({ From f28b9695d6e0379312b0b0dd3299bd3cd2241738 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 13:13:27 +0100 Subject: [PATCH 08/79] fix: moved metadata path --- src/server/services/communities.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/server/services/communities.js b/src/server/services/communities.js index d98099947..f5f686954 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -30,10 +30,6 @@ async function getGroupsService() { return res; } -const METADATA_PATH = path.resolve(__dirname, '../tc-communities'); - - - const getValidIds = async (METADATA_PATH) => { if (!isomorphy.isServerSide()) return []; @@ -177,6 +173,7 @@ getMetadata.maxage = 5 * 60 * 1000; // 5 min in ms. */ export async function getList(userGroupIds) { const list = []; + const METADATA_PATH = path.resolve(__dirname, '../tc-communities'); const validIds = await getValidIds(METADATA_PATH); return Promise.all( validIds.map(id => getMetadata(id).then((data) => { From 41ec4bb448c11aadd21416acd6da117ab0adc762 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 13:30:24 +0100 Subject: [PATCH 09/79] fix: lint --- src/server/index.js | 97 +++++++++++++++--------------- src/server/services/communities.js | 72 +++++++++++----------- 2 files changed, 86 insertions(+), 83 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index 5246b7303..a38efcf1a 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -43,6 +43,7 @@ global.atob = atob; const CMS_BASE_URL = `https://app.contentful.com/spaces/${config.SECRET.CONTENTFUL.SPACE_ID}`; const getTimestamp = async () => { + let timestamp; try { const filePath = path.resolve(__dirname, '../../.build-info'); if (!filePath.startsWith(path.resolve(__dirname))) { @@ -50,12 +51,12 @@ const getTimestamp = async () => { } const MAX_FILE_SIZE = 10 * 1024; // 10 KB max file size - const stats = await fs.stat(filePath); + const stats = await fs.promises.stat(filePath); if (stats.size > MAX_FILE_SIZE) { throw new Error('File is too large and may cause DoS issues'); } - const fileContent = await fs.readFile(filePath, 'utf-8'); + const fileContent = await fs.promises.readFile(filePath, 'utf-8'); let tsData; try { @@ -68,10 +69,12 @@ const getTimestamp = async () => { throw new Error('Timestamp is missing in the JSON file'); } - return moment(tsData.timestamp).valueOf(); + timestamp = moment(tsData.timestamp).valueOf(); } catch (err) { console.error('Error:', err.message); } + + return timestamp; }; const sw = `sw.js${process.env.NODE_ENV === 'production' ? '' : '?debug'}`; @@ -80,51 +83,49 @@ const swScope = '/challenges'; // we are currently only interested in improving const tcoPattern = new RegExp(/^tco\d{2}\.topcoder(?:-dev)?\.com$/i); const universalNavUrl = config.UNIVERSAL_NAV_URL; -const getExtraScripts = (ts) => { - return [ - ``, - ``, - ``, - ``, - ` - - `, - ] -} + }; + }).catch((err)=>{console.log('SW registration failed: ',err)}) + } + `, + ``, + ``, + ``, + ` + + `, +]; const MODE = process.env.BABEL_ENV; @@ -142,7 +143,7 @@ async function beforeRender(req, suggestedConfig) { await DoSSR(req, store, Application); -const ts = await getTimestamp(); + const ts = await getTimestamp(); return { configToInject: { ...suggestedConfig, EXCHANGE_RATES: rates }, diff --git a/src/server/services/communities.js b/src/server/services/communities.js index f5f686954..76e42b516 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -32,52 +32,54 @@ async function getGroupsService() { const getValidIds = async (METADATA_PATH) => { if (!isomorphy.isServerSide()) return []; - - const promise = new Promise(); const VALID_IDS = []; try { - await fs.readdir(METADATA_PATH, async (err, ids) => { - for (const id of ids) { - const uri = path.resolve(METADATA_PATH, id, 'metadata.json'); - - try { - await fs.access(uri); - - const stats = await fs.stat(uri); - const MAX_FILE_SIZE = 1 * 1024 * 1024; - if (stats.size > MAX_FILE_SIZE) { - console.warn(`Metadata file too large for ID: ${id}`); - continue; - } - - const meta = JSON.parse(await fs.readFile(uri, 'utf8')); - - if (Array.isArray(meta.subdomains)) { - meta.subdomains.forEach((subdomain) => { - if (typeof subdomain === 'string') { - SUBDOMAIN_COMMUNITY[subdomain] = id; - } else { - console.warn(`Invalid subdomain entry for ID: ${id}`); - } - }); - } - - VALID_IDS.push(id); - } catch (e) { - console.error(`Error processing metadata for ID: ${id}`, e.message); - promise.reject(`Error processing metadata for ID: ${id}`) + const ids = await fs.promises.readdir(METADATA_PATH); + const validationPromises = ids.map(async (id) => { + const uri = path.resolve(METADATA_PATH, id, 'metadata.json'); + + try { + // Check if the file exists + await fs.promises.access(uri); + + // Get file stats + const stats = await fs.promises.stat(uri); + const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1 MB + if (stats.size > MAX_FILE_SIZE) { + console.warn(`Metadata file too large for ID: ${id}`); + return null; // Exclude invalid ID + } + + // Parse and validate JSON + const meta = JSON.parse(await fs.promises.readFile(uri, 'utf8')); + + // Check if "subdomains" is a valid array + if (Array.isArray(meta.subdomains)) { + meta.subdomains.forEach((subdomain) => { + if (typeof subdomain === 'string') { + SUBDOMAIN_COMMUNITY[subdomain] = id; + } else { + console.warn(`Invalid subdomain entry for ID: ${id}`); + } + }); } - } - promise.resolve(VALID_IDS); + return id; + } catch (e) { + console.error(`Error processing metadata for ID: ${id}`, e.message); + return null; + } }); - return VALID_IDS; + const results = await Promise.all(validationPromises); + VALID_IDS = results.filter((id) => id !== null); } catch (err) { console.error(`Error reading metadata directory: ${METADATA_PATH}`, err.message); return []; } + + return VALID_IDS; }; /** From f04488f9f044b60887a23c98fd33ca8eb93e1288 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 13:37:33 +0100 Subject: [PATCH 10/79] fix: lint --- src/server/index.js | 4 ++-- src/server/services/communities.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index a38efcf1a..571773f66 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -83,7 +83,7 @@ const swScope = '/challenges'; // we are currently only interested in improving const tcoPattern = new RegExp(/^tco\d{2}\.topcoder(?:-dev)?\.com$/i); const universalNavUrl = config.UNIVERSAL_NAV_URL; -const getExtraScripts = (ts) => [ +const getExtraScripts = (ts) => ([ ` `, -]; +]); const MODE = process.env.BABEL_ENV; diff --git a/src/server/services/communities.js b/src/server/services/communities.js index 76e42b516..862e9d291 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -32,7 +32,7 @@ async function getGroupsService() { const getValidIds = async (METADATA_PATH) => { if (!isomorphy.isServerSide()) return []; - const VALID_IDS = []; + let VALID_IDS = []; try { const ids = await fs.promises.readdir(METADATA_PATH); @@ -41,7 +41,7 @@ const getValidIds = async (METADATA_PATH) => { try { // Check if the file exists - await fs.promises.access(uri); + await fs.promises.access(uri); // Get file stats const stats = await fs.promises.stat(uri); @@ -176,7 +176,7 @@ getMetadata.maxage = 5 * 60 * 1000; // 5 min in ms. export async function getList(userGroupIds) { const list = []; const METADATA_PATH = path.resolve(__dirname, '../tc-communities'); - const validIds = await getValidIds(METADATA_PATH); + const validIds = await getValidIds(METADATA_PATH); return Promise.all( validIds.map(id => getMetadata(id).then((data) => { if (!data.authorizedGroupIds From ef0c69a08073ee7452b494f36ea315e435402d54 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 18 Dec 2024 13:46:12 +0100 Subject: [PATCH 11/79] fix: lint --- src/server/index.js | 4 ++-- src/server/services/communities.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index 571773f66..16fb3fb5b 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -83,7 +83,7 @@ const swScope = '/challenges'; // we are currently only interested in improving const tcoPattern = new RegExp(/^tco\d{2}\.topcoder(?:-dev)?\.com$/i); const universalNavUrl = config.UNIVERSAL_NAV_URL; -const getExtraScripts = (ts) => ([ +const getExtraScripts = ts => [ ` `, -]); +]; const MODE = process.env.BABEL_ENV; diff --git a/src/server/services/communities.js b/src/server/services/communities.js index 862e9d291..c7b54d0f8 100644 --- a/src/server/services/communities.js +++ b/src/server/services/communities.js @@ -73,7 +73,7 @@ const getValidIds = async (METADATA_PATH) => { }); const results = await Promise.all(validationPromises); - VALID_IDS = results.filter((id) => id !== null); + VALID_IDS = results.filter(id => id !== null); } catch (err) { console.error(`Error reading metadata directory: ${METADATA_PATH}`, err.message); return []; From d89f6d15a4e1a2fec5767ea065781fdfbe09f549 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 18 Dec 2024 17:24:14 +0200 Subject: [PATCH 12/79] PM-197 - XSS poor validation error handling --- src/shared/components/Contentful/Article/Article.jsx | 2 +- src/shared/components/Gigs/GigApply/index.jsx | 6 +++--- src/shared/components/TopcoderHeader/Auth/index.jsx | 2 +- src/shared/components/tc-communities/AccessDenied/index.jsx | 2 +- src/shared/components/tc-communities/Footer/index.jsx | 4 ++-- src/shared/components/tc-communities/Header/index.jsx | 6 +++--- src/shared/containers/Dashboard/index.jsx | 2 +- src/shared/containers/challenge-detail/index.jsx | 2 +- src/shared/containers/tc-communities/Loader.jsx | 2 +- src/shared/utils/tc.js | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/shared/components/Contentful/Article/Article.jsx b/src/shared/components/Contentful/Article/Article.jsx index d15b08c26..772a0afb0 100644 --- a/src/shared/components/Contentful/Article/Article.jsx +++ b/src/shared/components/Contentful/Article/Article.jsx @@ -139,7 +139,7 @@ class Article extends React.Component { } = this.state || {}; let shareUrl; if (isomorphy.isClientSide()) { - shareUrl = encodeURIComponent(window.location.href); + shareUrl = encodeURIComponent(`${window.location.origin}${window.location.pathname}`); } const description = htmlToText.fromString( ReactDOMServer.renderToString(markdown(fields.content)), diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index a36919c2b..53d6b557b 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -36,7 +36,7 @@ export default function GigApply(props) { recruitProfile, auth, } = props; - const retUrl = window.location.href; + const retUrl = encodeURIComponent(`${window.location.origin}${window.location.pathname}`); const duration = getCustomField(job.custom_fields, 'Duration'); const isPlaced = _.find(_.isEmpty(recruitProfile) ? [] : recruitProfile.custom_fields, { field_id: 12 }); const fetchSkills = useMemo(() => _.debounce((inputValue, callback) => { @@ -353,9 +353,9 @@ export default function GigApply(props) {

You must be a Topcoder member to apply!

- Login + Login
-

Not a member? Register here.

+

Not a member? Register here.

diff --git a/src/shared/components/TopcoderHeader/Auth/index.jsx b/src/shared/components/TopcoderHeader/Auth/index.jsx index ab8c237b6..eee206a73 100644 --- a/src/shared/components/TopcoderHeader/Auth/index.jsx +++ b/src/shared/components/TopcoderHeader/Auth/index.jsx @@ -28,7 +28,7 @@ export default function Auth({ column }) { className="tc-btn-sm tc-btn-default" href={`${config.URL.AUTH}/member?utm_source=community-app-main`} onClick={(event) => { - const retUrl = encodeURIComponent(window.location.href); + const retUrl = encodeURIComponent(`${window.location.origin}${window.location.pathname}`); window.location = `${config.URL.AUTH}/member?retUrl=${retUrl}&utm_source=community-app-main`; event.preventDefault(); }} diff --git a/src/shared/components/tc-communities/AccessDenied/index.jsx b/src/shared/components/tc-communities/AccessDenied/index.jsx index a5730d6d9..32952604e 100644 --- a/src/shared/components/tc-communities/AccessDenied/index.jsx +++ b/src/shared/components/tc-communities/AccessDenied/index.jsx @@ -50,7 +50,7 @@ export default function AccessDenied(props) { className="tc-btn-md tc-btn-primary" href={`${config.URL.AUTH}/member?utm_source=${communityId}`} onClick={(event) => { - const retUrl = encodeURIComponent(window.location.href); + const retUrl = encodeURIComponent(`${window.location.origin}${window.location.pathname}`); window.location = `${config.URL.AUTH}/member?retUrl=${retUrl}&utm_source=${communityId}`; event.preventDefault(); }} diff --git a/src/shared/components/tc-communities/Footer/index.jsx b/src/shared/components/tc-communities/Footer/index.jsx index 8f0809a15..4299238d2 100644 --- a/src/shared/components/tc-communities/Footer/index.jsx +++ b/src/shared/components/tc-communities/Footer/index.jsx @@ -56,7 +56,7 @@ function Footer({ - - - - - )} -
- -

Account Role

-
-
- Access to Topcoder tools and applications are based on your account - role. If you change this setting, you will be required to sign out - of your account and login. -
-
-
- - - -
-
-
-
- - ); -}; - -MyPrimaryRole.propTypes = { - user: PT.shape().isRequired, - tokenV3: PT.string.isRequired, - updatePrimaryRole: PT.func.isRequired, -}; - - -export default withRouter(MyPrimaryRole); diff --git a/src/shared/components/Settings/Account/MyPrimaryRole/styles.scss b/src/shared/components/Settings/Account/MyPrimaryRole/styles.scss deleted file mode 100644 index 787ff5d4d..000000000 --- a/src/shared/components/Settings/Account/MyPrimaryRole/styles.scss +++ /dev/null @@ -1,254 +0,0 @@ -@import "../../style"; -@import "~styles/mixins"; - -.hide { - display: none; -} - -.form-container-default { - display: flex; - flex-direction: column; - - .form-default { - display: block; - - @include upto-sm { - display: none; - } - } - - .form-mobile { - display: none; - - @include upto-sm { - display: block; - - .row { - display: flex; - flex-direction: column; - } - } - } - - input { - @include roboto-regular; - - height: 40px; - font-size: 15px; - line-height: 20px; - font-weight: 400; - color: $tc-black; - border: 1px solid $tc-gray-20; - border-radius: $corner-radius * 2 $corner-radius * 2 $corner-radius * 2 $corner-radius * 2; - margin-bottom: 0; - } - - .form-field { - background: white; - color: black; - - &:disabled { - color: #b7b7b7; - } - - &.grey { - background-color: #fcfcfc; - color: #151516; - } - } -} - -.form-container { - padding: $pad-xxxxl; - background-color: $color-tc-white; - border-radius: 4px; - margin: $margin-sm 0 $margin-xxxxl 0; - - .account-form { - display: flex; - justify-content: space-between; - width: 100%; - - @media (max-width: 768px) { - flex-direction: column; - } - } -} - -.form-title { - @include barlow-semi-bold; - - font-size: 20px; - line-height: 22px; - color: inherit; - text-transform: uppercase; - padding-bottom: $pad-xxxxl; -} - -.form-content { - display: flex; - flex-wrap: wrap; - align-items: flex-start; -} - -.form-label { - flex: 0 0 calc(50% - 13px); - padding-right: 230px; - - @include roboto-regular; - - font-size: 16px; - line-height: 26px; - color: inherit; -} - -.form-body { - flex: 0 0 calc(50% + 13px); -} - -@include xs-to-md { - .form-container { - padding: $pad-xxl $pad-lg; - } - - .form-title { - @include barlow-semi-bold; - - font-size: 20px; - line-height: 22px; - padding-bottom: $pad-xxl; - } - - .form-label { - flex: 1 1 100%; - padding: 0; - margin-bottom: $margin-xxl; - font-size: 14px; - line-height: 20px; - } - - .form-body { - flex: 1 1 100%; - } - - .form-footer { - margin: 0; - } -} - -.nagModal { - display: flex; - flex-direction: column; - margin: 32px; - - @include xs-to-md { - flex-direction: column; - margin-top: 24px; - } - - .header { - display: flex; - align-items: flex-start; - justify-content: space-between; - border-bottom: 2px solid #e9e9e9; - - @include xs-to-md { - margin-top: 24px; - text-align: center; - } - - .title { - @include barlow-bold; - - color: $tco-black; - font-size: 22px; - font-weight: 600; - line-height: 26px; - margin-bottom: 12px; - text-transform: uppercase; - - @include xs-to-md { - text-align: center; - } - } - - .icon { - cursor: pointer; - } - } - - .description { - @include roboto-regular; - - font-weight: 400; - color: $tco-black; - font-size: 16px; - line-height: 24px; - margin-top: 24px; - - strong { - font-weight: 700; - } - - .badgeWrap { - display: flex; - justify-content: center; - margin-bottom: 12px; - } - - span span { - color: #137d60; - font-weight: bold; - } - } -} - -.container { - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); - border-radius: 8px; - min-width: 600px; - max-width: 700px; - - @include xs-to-sm { - width: 90%; - min-width: unset; - } -} - -.overlay { - background-color: #0c0c0c; - opacity: 0.85; -} - -.actionButtons { - display: flex; - align-items: center; - justify-content: flex-end; - margin-top: 32px; - padding-top: 24px; - border-top: 2px solid #e9e9e9; - - .primaryBtn { - background-color: #137d60; - border-radius: 24px; - color: #fff; - font-size: 13px; - font-weight: bolder; - text-decoration: none; - text-transform: uppercase; - line-height: 32px; - padding: 0 20px; - border: none; - outline: none; - display: flex; - - &:hover { - box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); - background-color: #0ab88a; - } - - @include xs-to-sm { - margin-bottom: 20px; - } - } -} diff --git a/src/shared/components/Settings/Account/Security/Modal/index.jsx b/src/shared/components/Settings/Account/Security/Modal/index.jsx deleted file mode 100644 index 3e33c982e..000000000 --- a/src/shared/components/Settings/Account/Security/Modal/index.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useEffect } from 'react'; -import PT from 'prop-types'; -import DiceImage from 'assets/images/account/security/dicelogosmall.png'; -import CloseButton from 'assets/images/account/security/green-close.svg'; - -import './style.scss'; - -export default function DiceModal({ - children, onCancel, leftButtonName, leftButtonDisabled, leftButtonClick, - rightButtonName, rightButtonDisabled, rightButtonClick, rightButtonHide, -}) { - useEffect(() => { - document.body.style.overflow = 'hidden'; - return () => { document.body.style.overflow = 'unset'; }; - }, []); - - return ( - ( - -
event.stopPropagation()} - > -
-
- diceid -
-
DICE ID authenticator setup
- -
-
-
- {children} -
-
-
-
- {leftButtonName} -
- {!rightButtonHide && ( -
- {rightButtonName} -
- )} -
-
-
- - ) - ); -} -DiceModal.defaultProps = { - children: null, - leftButtonDisabled: false, - rightButtonDisabled: false, - rightButtonHide: false, -}; -DiceModal.propTypes = { - children: PT.node, - onCancel: PT.func.isRequired, - leftButtonName: PT.string.isRequired, - leftButtonDisabled: PT.bool, - leftButtonClick: PT.func.isRequired, - rightButtonName: PT.string.isRequired, - rightButtonDisabled: PT.bool, - rightButtonClick: PT.func.isRequired, - rightButtonHide: PT.bool, -}; diff --git a/src/shared/components/Settings/Account/Security/Modal/style.scss b/src/shared/components/Settings/Account/Security/Modal/style.scss deleted file mode 100644 index 1f972a52e..000000000 --- a/src/shared/components/Settings/Account/Security/Modal/style.scss +++ /dev/null @@ -1,162 +0,0 @@ -@import "../../../style"; -@import "~styles/mixins"; - -.overlay { - background: #0c0c0c; - border: none; - height: 100%; - left: 0; - opacity: 0.85; - outline: none; - position: fixed; - top: 0; - width: 100%; - z-index: 950; -} - -.container { - background: #fff; - position: fixed; - max-width: 1000px; - height: 752px; - max-height: 99%; - top: 50%; - left: 50%; - padding: 32px; - box-shadow: 0 0 1px 5px rgba(0, 0, 0, 0.2); - border-radius: 8px; - overflow: auto; - transform: translate(-50%, -50%); - z-index: 951; - display: flex; - flex-direction: column; - align-items: flex-start; - - .header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - margin-bottom: 24px; - - .icon-wrapper { - display: flex; - align-items: center; - justify-content: center; - width: 150px; - height: 40px; - background: #fff; - border-radius: 4px; - margin-right: $margin-md; - - img { - display: block; - } - } - - .title { - @include barlow-semi-bold; - - flex: 1; - font-size: 22px; - line-height: 26px; - color: $tco-black; - text-transform: uppercase; - } - - .close-button { - cursor: pointer; - height: 15px; - width: 15px; - } - } - - .body { - flex: 1; - width: 100%; - display: flex; - flex-direction: column; - margin-bottom: 24px; - } - - .divider { - width: 100%; - min-height: 2px; - background-color: #e9e9e9; - border-radius: 1px; - margin-bottom: 24px; - } - - .footer { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - width: 100%; - - .left-button { - @include roboto-bold; - - text-align: center; - margin: 0; - height: 48px; - font-size: 16px; - line-height: 21px; - letter-spacing: 0.008em; - text-transform: uppercase; - color: $color-turq-160; - padding: 12px 24px; - background: $tc-white; - border: 2px solid $color-turq-160; - border-radius: 50px; - cursor: pointer; - - &:focus { - outline: none; - } - - &:focus-visible { - outline: none; - } - - &.disabled { - background: #f4f4f4; - color: #767676; - border: none; - line-height: 24px; - pointer-events: none; - } - } - - .right-button { - @include roboto-bold; - - text-align: center; - margin: 0; - height: 48px; - font-size: 16px; - line-height: 24px; - letter-spacing: 0.008em; - text-transform: uppercase; - color: $tc-white; - padding: 12px 24px; - background: $color-turq-160; - border-radius: 50px; - cursor: pointer; - - &:focus { - outline: none; - } - - &:focus-visible { - outline: none; - } - - &.disabled { - background: #f4f4f4; - color: #767676; - pointer-events: none; - } - } - } -} diff --git a/src/shared/components/Settings/Account/Security/VerificationListener/index.jsx b/src/shared/components/Settings/Account/Security/VerificationListener/index.jsx deleted file mode 100644 index 26e0ee9e5..000000000 --- a/src/shared/components/Settings/Account/Security/VerificationListener/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect, useCallback } from 'react'; -import PT from 'prop-types'; - -export default function VerificationListener({ - event, callback, origin, type, onProcessing, startType, -}) { - const messageHandler = useCallback((e) => { - if (e.origin === origin && e.data && e.data.type) { - if (e.data.type === startType) { - onProcessing(); - } else if (e.data.type === type) { - callback(e.data); - } - } - }, [origin, type, startType]); - - useEffect(() => { - window.addEventListener(event, messageHandler); - return () => window.removeEventListener(event, messageHandler); - }, [event, messageHandler]); - - return false; -} - -VerificationListener.propTypes = { - event: PT.string.isRequired, - callback: PT.func.isRequired, - origin: PT.string.isRequired, - type: PT.string.isRequired, - onProcessing: PT.func.isRequired, - startType: PT.string.isRequired, -}; diff --git a/src/shared/components/Settings/Account/Security/index.jsx b/src/shared/components/Settings/Account/Security/index.jsx deleted file mode 100644 index 796cf1beb..000000000 --- a/src/shared/components/Settings/Account/Security/index.jsx +++ /dev/null @@ -1,430 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import PT from 'prop-types'; -import _ from 'lodash'; -import { config } from 'topcoder-react-utils'; -import QRCode from 'react-qr-code'; -import { SettingBannerV2 as Collapse } from 'components/Settings/SettingsBanner'; -import Tooltip from 'components/Tooltip'; -import MfaImage from 'assets/images/account/security/mfa.svg'; -import DiceLogo from 'assets/images/account/security/dicelogo.png'; -import DiceLogoBig from 'assets/images/account/security/dicelogobig.png'; -import GooglePlay from 'assets/images/account/security/google-play.png'; -import AppleStore from 'assets/images/account/security/apple-store.svg'; -import UnsuccessfulIcon from 'assets/images/account/security/unsuccessful.svg'; -import TooltipInfo from 'assets/images/tooltip-info.svg'; -import Modal from './Modal'; -import VerificationListener from './VerificationListener'; - - -import './styles.scss'; - -export default function Security({ - usermfa, updateUser2fa, updateUserDice, getNewDiceConnection, - getDiceConnection, tokenV3, handle, emailAddress, -}) { - const [setupStep, setSetupStep] = useState(-1); - const [isConnVerifyRunning, setIsConnVerifyRunning] = useState(false); - const [connVerifyCounter, setConnVerifyCounter] = useState(0); - const [isVerificationProcessing, setIsVerificationProcessing] = useState(false); - const diceVerifyUrl = config.DICE_VERIFY_URL; - - const diceTip = ( -
-

Please reach out to support@topcoder.com for deactivating Dice ID

-
- ); - const useInterval = (callback, delay) => { - const savedCallback = useRef(); - - useEffect(() => { - savedCallback.current = callback; - }, [callback]); - - // eslint-disable-next-line consistent-return - useEffect(() => { - function tick() { - savedCallback.current(); - } - if (delay !== null) { - const id = setInterval(tick, delay); - return () => clearInterval(id); - } - setConnVerifyCounter(0); - }, [delay]); - }; - - const getMfaOption = () => { - const mfaEnabled = _.get(usermfa, 'user2fa.mfaEnabled'); - if (mfaEnabled) return true; - return false; - }; - - const getDiceOption = () => { - const diceEnabled = _.get(usermfa, 'user2fa.diceEnabled'); - if (diceEnabled) return true; - return false; - }; - - const mfaChecked = getMfaOption(); - const diceChecked = getDiceOption(); - const userId = _.get(usermfa, 'user2fa.userId'); - const diceConnection = _.get(usermfa, 'diceConnection'); - - const onUpdateMfaOption = () => { - if (usermfa.updatingUser2fa) { - return; - } - updateUser2fa(userId, !mfaChecked, tokenV3); - }; - - const goToConnection = () => { - if (mfaChecked && !usermfa.gettingNewDiceConnection) { - getNewDiceConnection(userId, tokenV3); - } - setSetupStep(1); - setIsConnVerifyRunning(true); - }; - - const getConnectionAccepted = () => { - if (diceConnection.accepted) return true; - return false; - }; - - const openSetup = () => { - setSetupStep(0); - }; - - const closeSetup = () => { - setSetupStep(-1); - setIsVerificationProcessing(false); - }; - - const verifyConnection = () => { - if (getConnectionAccepted() || usermfa.diceConnectionError) { - setIsConnVerifyRunning(false); - } else if (!usermfa.gettingDiceConnection && diceConnection.id) { - if (connVerifyCounter >= 36) { - closeSetup(); - } else { - getDiceConnection(userId, diceConnection.id, tokenV3); - setConnVerifyCounter(connVerifyCounter + 1); - } - } - }; - - const goToVerification = () => { - if (!getConnectionAccepted()) { - return; - } - setSetupStep(2); - }; - - const verificationCallback = (data) => { - if (data.success) { - const userEmail = _.get(data, 'user.profile.Email'); - if (!_.isUndefined(userEmail) && _.lowerCase(userEmail) === _.lowerCase(emailAddress)) { - updateUserDice(userId, true, tokenV3); - setSetupStep(3); - } else { - setSetupStep(4); - } - } else { - setSetupStep(4); - } - }; - - const onStartProcessing = () => { - setIsVerificationProcessing(true); - }; - - useInterval(verifyConnection, setupStep === 1 && isConnVerifyRunning ? 5000 : null); - - const getVerificationStepTitle = () => { - if (isVerificationProcessing) { - return 'Processing...'; - } - return 'STEP 3 OF 3'; - }; - - const getVerificationStepText = () => { - if (isVerificationProcessing) { - return 'Please wait while your credentials are validated.'; - } - return 'Scan the following DICE ID QR Code in your DICE ID mobile application to confirm your credential.'; - }; - - const setupStepNodes = [ - -
-
- STEP 1 OF 3 -
-
- First, please download the DICE ID App from the - Google Play Store or the iOS App Store. -
-
-
- Google Play Store - -
-
- - -
-
-
- After you have downloaded and installed the mobile app, - make sure to complete the configuration process. - When ready, click next below. -
-
-
, - -
-
- STEP 2 OF 3 -
-
- Scan the following DICE ID QR Code in your DICE ID mobile application. -
-
- {diceConnection.connection - ? - : 'Loading'} -
-
- Once the connection is established, the service will offer you a Verifiable Credential. -
Press the ACCEPT button in your DICE ID App. -
If you DECLINE the invitation, please try again after 5 minutes. -
-
-
, - {}} - rightButtonHide - > -
-
- {getVerificationStepTitle()} -
-
- {getVerificationStepText()} -
-