Skip to content
Open
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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ workflows:
branches:
only:
- develop
- PM-4482
- PM-4975

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
18 changes: 16 additions & 2 deletions src/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,20 @@ function convertBigIntDeep (value) {
}
return value
}

/**
* Validate that a member handle contains only permitted characters.
* Handles with special chars like <, >, ?, spaces, or raw Unicode
* that were created via legacy systems should return a clean 404.
* @param {String} handle the member handle
* @throws {NotFoundError} if handle contains invalid characters
*/
function validateHandle (handle) {
// Topcoder handles: 2–50 chars, letters, digits, hyphens, underscores, periods only
const VALID_HANDLE_RE = /^[a-zA-Z0-9\-_.]{2,50}$/
if (!VALID_HANDLE_RE.test(handle)) {
throw new errors.NotFoundError(`Member with handle: "${handle}" doesn't exist`)
}
}
module.exports = {
wrapExpress,
autoWrapExpress,
Expand Down Expand Up @@ -661,5 +674,6 @@ module.exports = {
secureMemberAddressData,
truncateLastName,
bigIntToNumber,
convertBigIntDeep
convertBigIntDeep,
validateHandle
}
11 changes: 11 additions & 0 deletions src/controllers/MemberController.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const service = require('../services/MemberService')
* @param {Object} res the response
*/
async function getMember (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.getMember(req.authUser, req.params.handle, req.query)
res.send(result)
}
Expand All @@ -18,6 +19,7 @@ async function getMember (req, res) {
* @param {Object} res the response
*/
async function getProfileCompleteness (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.getProfileCompleteness(req.authUser, req.params.handle, req.query)
res.send(result)
}
Expand All @@ -38,6 +40,7 @@ async function getMemberUserIdSignature (req, res) {
* @param {Object} res the response
*/
async function getMemberSkill (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.getMemberSkill(req.authUser, req.params.handle, req.params.skillid)
res.send(result)
}
Expand All @@ -48,6 +51,7 @@ async function getMemberSkill (req, res) {
* @param {Object} res the response
*/
async function updateMember (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.updateMember(req.authUser, req.params.handle, req.query, req.body)
res.send(result)
}
Expand All @@ -58,6 +62,7 @@ async function updateMember (req, res) {
* @param {Object} res the response
*/
async function updateHandle (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.updateHandle(req.authUser, req.params.handle, req.query, req.body)
res.send(result)
}
Expand All @@ -68,6 +73,7 @@ async function updateHandle (req, res) {
* @param {Object} res the response
*/
async function verifyEmail (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.verifyEmail(req.authUser, req.params.handle, req.query)
res.send(result)
}
Expand All @@ -78,6 +84,7 @@ async function verifyEmail (req, res) {
* @param {Object} res the response
*/
async function uploadPhoto (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.uploadPhoto(req.authUser, req.params.handle, req.files)
res.send(result)
}
Expand All @@ -88,6 +95,7 @@ async function uploadPhoto (req, res) {
* @param {Object} res the response
*/
async function deleteMember (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.deleteMember(req.authUser, req.params.handle, req.body)
res.send(result)
}
Expand All @@ -98,6 +106,7 @@ async function deleteMember (req, res) {
* @param {Object} res the response
*/
async function confirmProfileData (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.confirmProfileData(req.authUser, req.params.handle)
res.send(result)
}
Expand All @@ -108,6 +117,7 @@ async function confirmProfileData (req, res) {
* @param {Object} res the response
*/
async function downloadProfile (req, res) {
const handle = decodeURIComponent(req.params.handle)
const pdfStream = await service.downloadProfile(req.authUser, req.params.handle)
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', `attachment; filename="topcoder-profile-${req.params.handle}.pdf"`)
Expand All @@ -120,6 +130,7 @@ async function downloadProfile (req, res) {
* @param {Object} res the response
*/
async function getMemberSendgridEmails (req, res) {
const handle = decodeURIComponent(req.params.handle)
const result = await service.getMemberSendgridEmails(req.authUser, req.params.handle)
res.send(result)
}
Expand Down
22 changes: 14 additions & 8 deletions src/services/MemberService.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ function omitMemberAttributes (currentUser, mb) {
const hasSensitiveDataRole = helper.hasSensitiveDataRole(currentUser)
const isM2M = currentUser && currentUser.isMachine
const isSelf = currentUser && currentUser.handle && mb.handleLower &&
currentUser.handle.trim().toLowerCase() === mb.handleLower.trim().toLowerCase()
currentUser.handle.toLowerCase() === mb.handleLower.toLowerCase()
const canSeeIdentityVerified = isM2M || hasSensitiveDataRole || isSelf
const canSeeRecentActivity = isM2M || hasSensitiveDataRole || isSelf
const canSeeFullAddress = canManageMember || hasSensitiveDataRole
Expand Down Expand Up @@ -202,6 +202,7 @@ function getCountryNameFromCode (isoCode3) {
* @returns {Object} the member profile data
*/
async function getMemberData (handle, query, allowedFields = MEMBER_FIELDS) {
helper.validateHandle(handle)
// validate and parse query parameter
const selectFields = helper.parseCommaSeparatedString(query.fields, allowedFields) || allowedFields

Expand Down Expand Up @@ -251,7 +252,7 @@ async function getMember (currentUser, handle, query) {
const hasSensitiveDataRole = helper.hasSensitiveDataRole(currentUser)
const isM2M = currentUser && currentUser.isMachine
const isSelf = currentUser && currentUser.handle &&
currentUser.handle.trim().toLowerCase() === handle.trim().toLowerCase()
currentUser.handle.toLowerCase() === handle.toLowerCase()

const canSeePhones = isM2M || hasSensitiveDataRole || isSelf
const canSeeRecentActivity = isM2M || hasSensitiveDataRole || isSelf
Expand Down Expand Up @@ -345,7 +346,7 @@ getMember.schema = {
handle: Joi.string().required(),
query: Joi.object().keys({
fields: Joi.string()
})
}).unknown(true)
}

/**
Expand All @@ -369,6 +370,7 @@ function buildSendgridEmailActivityQuery (email, startTime, endTime) {
* @returns {Array} up to the most recent SendGrid message activity records
*/
async function getMemberSendgridEmails (currentUser, handle) {
helper.validateHandle(handle)
if (!currentUser || (!currentUser.isMachine && !helper.hasAdminRole(currentUser))) {
throw new errors.ForbiddenError('You are not allowed to view SendGrid email activity.')
}
Expand Down Expand Up @@ -586,7 +588,7 @@ getProfileCompleteness.schema = {
query: Joi.object().keys({
fields: Joi.string(),
toast: Joi.string()
})
}).unknown(true)
}

/**
Expand All @@ -610,7 +612,7 @@ getMemberUserIdSignature.schema = {
currentUser: Joi.any(),
query: Joi.object().keys({
type: Joi.string().valid('userflow').required()
}).required()
}).unknown(true).required()
}

/**
Expand All @@ -622,6 +624,7 @@ getMemberUserIdSignature.schema = {
* @returns {Object} the updated member data
*/
async function updateMember (currentUser, handle, query, data) {
helper.validateHandle(handle)
const operatorId = currentUser.userId || currentUser.sub
const member = await helper.getMemberByHandle(handle)
// check authorization
Expand Down Expand Up @@ -778,7 +781,7 @@ updateMember.schema = {
handle: Joi.string().required(),
query: Joi.object().keys({
fields: Joi.string()
}),
}).unknown(true),
data: Joi.object().keys({
handle: Joi.forbidden(),
handleLower: Joi.forbidden(),
Expand Down Expand Up @@ -820,6 +823,7 @@ updateMember.schema = {
* @returns {Object} the updated member data
*/
async function updateHandle (currentUser, handle, query, data) {
helper.validateHandle(handle)
const operatorId = currentUser.userId || currentUser.sub
const member = await helper.getMemberByHandle(handle)

Expand Down Expand Up @@ -925,7 +929,7 @@ updateHandle.schema = {
handle: Joi.string().required(),
query: Joi.object().keys({
fields: Joi.string()
}),
}).unknown(true),
data: Joi.object().keys({
newHandle: Joi.string().required()
}).required()
Expand Down Expand Up @@ -988,7 +992,7 @@ verifyEmail.schema = {
handle: Joi.string().required(),
query: Joi.object().keys({
token: Joi.string().required()
}).required()
}).unknown(true).required()
}

/**
Expand Down Expand Up @@ -1070,6 +1074,7 @@ async function uploadPhoto (currentUser, handle, files) {
* @returns {Object} the deletion result
*/
async function deleteMember (currentUser, handle, data) {
helper.validateHandle(handle)
if (!currentUser || (!currentUser.isMachine && !helper.hasAdminRole(currentUser))) {
throw new errors.ForbiddenError('You are not allowed to delete the member.')
}
Expand Down Expand Up @@ -1939,6 +1944,7 @@ async function aggregatePDFData (currentUser, handle) {
* @returns {Stream} PDF stream
*/
async function downloadProfile (currentUser, handle) {
helper.validateHandle(handle)
// Validate handle exists
const member = await helper.getMemberByHandle(handle)

Expand Down
6 changes: 3 additions & 3 deletions src/services/MemberTraitService.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async function getTraits (currentUser, handle, query) {
const hasSensitiveDataRole = helper.hasSensitiveDataRole(currentUser)
const isM2M = currentUser && currentUser.isMachine
const isSelf = currentUser && currentUser.handle &&
currentUser.handle.trim().toLowerCase() === handle.trim().toLowerCase()
currentUser.handle.toLowerCase() === handle.toLowerCase()

// can read private personalisation info on a member
const canReadPrivate = isM2M || hasSensitiveDataRole || isSelf
Expand Down Expand Up @@ -284,7 +284,7 @@ getTraits.schema = {
query: Joi.object().keys({
traitIds: Joi.string(),
fields: Joi.string()
})
}).unknown(true)
}

/**
Expand Down Expand Up @@ -659,7 +659,7 @@ removeTraits.schema = {
handle: Joi.string().required(),
query: Joi.object().keys({
traitIds: Joi.string() // if not provided, then all member traits are removed
})
}).unknown(true)
}
/**
* This function is used to calculate a deduction to the skill score used in the talent search
Expand Down
Loading