Skip to content

Commit

Permalink
Merge pull request #7022 from topcoder-platform/pm-199
Browse files Browse the repository at this point in the history
PM-199, PM-209 Denial of service fix
  • Loading branch information
hentrymartin authored Jan 10, 2025
2 parents df4ee6f + 4de7291 commit 7f57db6
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 23 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 38 additions & 5 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,48 @@ 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 () => {
let timestamp;
try {
const filePath = path.resolve(__dirname, '../../.build-info');
if (!filePath.startsWith(path.resolve(__dirname, '../../'))) {
throw new Error('Invalid file path detected');
}

const MAX_FILE_SIZE = 10 * 1024; // 10 KB max file size
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.promises.readFile(filePath, 'utf-8');

let tsData;
try {
tsData = JSON.parse(fileContent);
} catch (parseErr) {
throw new Error('Invalid JSON format in file');
}

if (!tsData || !tsData.timestamp) {
throw new Error('Timestamp is missing in the JSON file');
}

timestamp = moment(tsData.timestamp).valueOf();
} catch (err) {
console.error('Error:', err.message);
}

return timestamp;
};

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 = [
const getExtraScripts = ts => [
`<script type="application/javascript">
if('serviceWorker' in navigator){
navigator.serviceWorker.register('${swScope}/${sw}', {scope: '${swScope}'}).then(
Expand Down Expand Up @@ -112,9 +143,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,
};
}
Expand Down
71 changes: 53 additions & 18 deletions src/server/services/communities.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,57 @@ async function getGroupsService() {
return res;
}

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');
const getValidIds = async (METADATA_PATH) => {
if (!isomorphy.isServerSide()) return [];
let VALID_IDS = [];

try {
const meta = JSON.parse(fs.readFileSync(uri, 'utf8'));
if (meta.subdomains) {
meta.subdomains.forEach((subdomain) => {
SUBDOMAIN_COMMUNITY[subdomain] = id;
});
}
return true;
} catch (e) {
return false;
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}`);
}
});
}

return id;
} catch (e) {
console.error(`Error processing metadata for ID: ${id}`, e.message);
return null;
}
});

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;
};

/**
* Given an array of group IDs, returns an array containing IDs of all those
Expand Down Expand Up @@ -140,10 +173,12 @@ 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 METADATA_PATH = path.resolve(__dirname, '../tc-communities');
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({
Expand Down

0 comments on commit 7f57db6

Please sign in to comment.