Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PM-199, PM-209 Denial of service fix #7022

Merged
merged 9 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
jmgasper marked this conversation as resolved.
Show resolved Hide resolved
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
jmgasper marked this conversation as resolved.
Show resolved Hide resolved
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
Loading