From 414987ae2c7b6c27856f2ed8fdcaa647d99fffd3 Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Tue, 18 Jul 2023 12:27:45 +0200 Subject: [PATCH] Merge pull request from GHSA-6c37-r62q-7xf4 --- api-server/src/server/boot/settings.js | 42 ++++++- .../src/server/boot_tests/settings.test.js | 111 ++++++++++++++++++ 2 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 api-server/src/server/boot_tests/settings.test.js diff --git a/api-server/src/server/boot/settings.js b/api-server/src/server/boot/settings.js index 0fc22857780..3fadb488a8e 100644 --- a/api-server/src/server/boot/settings.js +++ b/api-server/src/server/boot/settings.js @@ -234,11 +234,43 @@ const updatePrivacyTerms = (req, res, next) => { ); }; -function updateMySocials(...args) { - const buildUpdate = body => - _.pick(body, ['githubProfile', 'linkedin', 'twitter', 'website']); - const validate = update => - Object.values(update).every(x => typeof x === 'string'); +const allowedSocialsAndDomains = { + githubProfile: 'github.com', + linkedin: 'linkedin.com', + twitter: 'twitter.com', + website: '' +}; + +const socialVals = Object.keys(allowedSocialsAndDomains); + +export function updateMySocials(...args) { + const buildUpdate = body => _.pick(body, socialVals); + const validate = update => { + // Socials should point to their respective domains + // or be empty strings + return Object.keys(update).every(key => { + const val = update[key]; + if (val === '') { + return true; + } + if (key === 'website') { + return isURL(val, { require_protocol: true }); + } + + const domain = allowedSocialsAndDomains[key]; + + try { + const url = new URL(val); + const topDomain = url.hostname.split('.').slice(-2); + if (topDomain.length === 2) { + return topDomain.join('.') === domain; + } + return false; + } catch (e) { + return false; + } + }); + }; createUpdateUserProperties( buildUpdate, validate, diff --git a/api-server/src/server/boot_tests/settings.test.js b/api-server/src/server/boot_tests/settings.test.js new file mode 100644 index 00000000000..f51f61a4f3d --- /dev/null +++ b/api-server/src/server/boot_tests/settings.test.js @@ -0,0 +1,111 @@ +import { updateMySocials } from '../boot/settings'; + +export const mockReq = opts => { + const req = {}; + return { ...req, ...opts }; +}; + +export const mockRes = opts => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.redirect = jest.fn().mockReturnValue(res); + res.set = jest.fn().mockReturnValue(res); + res.clearCookie = jest.fn().mockReturnValue(res); + res.cookie = jest.fn().mockReturnValue(res); + return { ...res, ...opts }; +}; + +describe('boot/settings', () => { + describe('updateMySocials', () => { + it('does not allow non-github domain in GitHub social', () => { + const req = mockReq({ + user: {}, + body: { + githubProfile: 'https://www.almost-github.com' + } + }); + const res = mockRes(); + const next = jest.fn(); + updateMySocials(req, res, next); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('does not allow non-linkedin domain in LinkedIn social', () => { + const req = mockReq({ + user: {}, + body: { + linkedin: 'https://www.freecodecamp.org' + } + }); + const res = mockRes(); + const next = jest.fn(); + updateMySocials(req, res, next); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('does not allow non-twitter domain in Twitter social', () => { + const req = mockReq({ + user: {}, + body: { + twitter: 'https://www.freecodecamp.org' + } + }); + const res = mockRes(); + const next = jest.fn(); + updateMySocials(req, res, next); + expect(res.status).toHaveBeenCalledWith(403); + }); + + it('allows empty string in any social', () => { + const req = mockReq({ + user: { + updateAttributes: (_, cb) => cb() + }, + body: { + twitter: '', + linkedin: '', + githubProfile: '', + website: '' + } + }); + const res = mockRes(); + const next = jest.fn(); + updateMySocials(req, res, next); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('allows any valid link in website social', () => { + const req = mockReq({ + user: { + updateAttributes: (_, cb) => cb() + }, + body: { + website: 'https://www.freecodecamp.org' + } + }); + const res = mockRes(); + const next = jest.fn(); + updateMySocials(req, res, next); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it('allows valid links with sub-domains to pass', () => { + const req = mockReq({ + user: { + updateAttributes: (_, cb) => cb() + }, + body: { + githubProfile: 'https://www.gist.github.com', + linkedin: 'https://www.linkedin.com/freecodecamp', + twitter: 'https://www.twitter.com/freecodecamp', + website: 'https://www.example.freecodecamp.org' + } + }); + const res = mockRes(); + const next = jest.fn(); + updateMySocials(req, res, next); + expect(res.status).toHaveBeenCalledWith(200); + }); + }); +});