Skip to content

Commit 7f57db6

Browse files
authored
Merge pull request #7022 from topcoder-platform/pm-199
PM-199, PM-209 Denial of service fix
2 parents df4ee6f + 4de7291 commit 7f57db6

File tree

3 files changed

+92
-23
lines changed

3 files changed

+92
-23
lines changed

.circleci/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ workflows:
360360
- develop
361361
- TOP-1390
362362
- PM-191-2
363+
- pm-199
363364
# This is alternate dev env for parallel testing
364365
# Deprecate this workflow due to beta env shutdown
365366
# https://topcoder.atlassian.net/browse/CORE-251

src/server/index.js

+38-5
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,48 @@ global.atob = atob;
4242

4343
const CMS_BASE_URL = `https://app.contentful.com/spaces/${config.SECRET.CONTENTFUL.SPACE_ID}`;
4444

45-
let ts = path.resolve(__dirname, '../../.build-info');
46-
ts = JSON.parse(fs.readFileSync(ts));
47-
ts = moment(ts.timestamp).valueOf();
45+
const getTimestamp = async () => {
46+
let timestamp;
47+
try {
48+
const filePath = path.resolve(__dirname, '../../.build-info');
49+
if (!filePath.startsWith(path.resolve(__dirname, '../../'))) {
50+
throw new Error('Invalid file path detected');
51+
}
52+
53+
const MAX_FILE_SIZE = 10 * 1024; // 10 KB max file size
54+
const stats = await fs.promises.stat(filePath);
55+
if (stats.size > MAX_FILE_SIZE) {
56+
throw new Error('File is too large and may cause DoS issues');
57+
}
58+
59+
const fileContent = await fs.promises.readFile(filePath, 'utf-8');
60+
61+
let tsData;
62+
try {
63+
tsData = JSON.parse(fileContent);
64+
} catch (parseErr) {
65+
throw new Error('Invalid JSON format in file');
66+
}
67+
68+
if (!tsData || !tsData.timestamp) {
69+
throw new Error('Timestamp is missing in the JSON file');
70+
}
71+
72+
timestamp = moment(tsData.timestamp).valueOf();
73+
} catch (err) {
74+
console.error('Error:', err.message);
75+
}
76+
77+
return timestamp;
78+
};
4879

4980
const sw = `sw.js${process.env.NODE_ENV === 'production' ? '' : '?debug'}`;
5081
const swScope = '/challenges'; // we are currently only interested in improving challenges pages
5182

5283
const tcoPattern = new RegExp(/^tco\d{2}\.topcoder(?:-dev)?\.com$/i);
5384
const universalNavUrl = config.UNIVERSAL_NAV_URL;
5485

55-
const EXTRA_SCRIPTS = [
86+
const getExtraScripts = ts => [
5687
`<script type="application/javascript">
5788
if('serviceWorker' in navigator){
5889
navigator.serviceWorker.register('${swScope}/${sw}', {scope: '${swScope}'}).then(
@@ -112,9 +143,11 @@ async function beforeRender(req, suggestedConfig) {
112143

113144
await DoSSR(req, store, Application);
114145

146+
const ts = await getTimestamp();
147+
115148
return {
116149
configToInject: { ...suggestedConfig, EXCHANGE_RATES: rates },
117-
extraScripts: EXTRA_SCRIPTS,
150+
extraScripts: getExtraScripts(ts),
118151
store,
119152
};
120153
}

src/server/services/communities.js

+53-18
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,57 @@ async function getGroupsService() {
3030
return res;
3131
}
3232

33-
const METADATA_PATH = path.resolve(__dirname, '../tc-communities');
34-
const VALID_IDS = isomorphy.isServerSide()
35-
&& fs.readdirSync(METADATA_PATH).filter((id) => {
36-
/* Here we check which ids are correct, and also popuate SUBDOMAIN_COMMUNITY
37-
* map. */
38-
const uri = path.resolve(METADATA_PATH, id, 'metadata.json');
33+
const getValidIds = async (METADATA_PATH) => {
34+
if (!isomorphy.isServerSide()) return [];
35+
let VALID_IDS = [];
36+
3937
try {
40-
const meta = JSON.parse(fs.readFileSync(uri, 'utf8'));
41-
if (meta.subdomains) {
42-
meta.subdomains.forEach((subdomain) => {
43-
SUBDOMAIN_COMMUNITY[subdomain] = id;
44-
});
45-
}
46-
return true;
47-
} catch (e) {
48-
return false;
38+
const ids = await fs.promises.readdir(METADATA_PATH);
39+
const validationPromises = ids.map(async (id) => {
40+
const uri = path.resolve(METADATA_PATH, id, 'metadata.json');
41+
42+
try {
43+
// Check if the file exists
44+
await fs.promises.access(uri);
45+
46+
// Get file stats
47+
const stats = await fs.promises.stat(uri);
48+
const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1 MB
49+
if (stats.size > MAX_FILE_SIZE) {
50+
console.warn(`Metadata file too large for ID: ${id}`);
51+
return null; // Exclude invalid ID
52+
}
53+
54+
// Parse and validate JSON
55+
const meta = JSON.parse(await fs.promises.readFile(uri, 'utf8'));
56+
57+
// Check if "subdomains" is a valid array
58+
if (Array.isArray(meta.subdomains)) {
59+
meta.subdomains.forEach((subdomain) => {
60+
if (typeof subdomain === 'string') {
61+
SUBDOMAIN_COMMUNITY[subdomain] = id;
62+
} else {
63+
console.warn(`Invalid subdomain entry for ID: ${id}`);
64+
}
65+
});
66+
}
67+
68+
return id;
69+
} catch (e) {
70+
console.error(`Error processing metadata for ID: ${id}`, e.message);
71+
return null;
72+
}
73+
});
74+
75+
const results = await Promise.all(validationPromises);
76+
VALID_IDS = results.filter(id => id !== null);
77+
} catch (err) {
78+
console.error(`Error reading metadata directory: ${METADATA_PATH}`, err.message);
79+
return [];
4980
}
50-
});
81+
82+
return VALID_IDS;
83+
};
5184

5285
/**
5386
* Given an array of group IDs, returns an array containing IDs of all those
@@ -140,10 +173,12 @@ getMetadata.maxage = 5 * 60 * 1000; // 5 min in ms.
140173
* @return {Promise} Resolves to the array of community data objects. Each of
141174
* the objects indludes only the most important data on the community.
142175
*/
143-
export function getList(userGroupIds) {
176+
export async function getList(userGroupIds) {
144177
const list = [];
178+
const METADATA_PATH = path.resolve(__dirname, '../tc-communities');
179+
const validIds = await getValidIds(METADATA_PATH);
145180
return Promise.all(
146-
VALID_IDS.map(id => getMetadata(id).then((data) => {
181+
validIds.map(id => getMetadata(id).then((data) => {
147182
if (!data.authorizedGroupIds
148183
|| _.intersection(data.authorizedGroupIds, userGroupIds).length) {
149184
list.push({

0 commit comments

Comments
 (0)