fix(api): reject social urls with invalid domains (#55595)

This commit is contained in:
Oliver Eyton-Williams
2024-07-25 17:49:34 +02:00
committed by GitHub
parent cda00d4770
commit ee37d563f7
2 changed files with 86 additions and 3 deletions

View File

@@ -11,7 +11,11 @@ import {
import { formatMessage } from '../plugins/redirect-with-message';
import { createUserInput } from '../utils/create-user';
import { API_LOCATION, HOME_LOCATION } from '../utils/env';
import { isPictureWithProtocol, getWaitMessage } from './settings';
import {
isPictureWithProtocol,
getWaitMessage,
validateSocialUrl
} from './settings';
const baseProfileUI = {
isLocked: false,
@@ -827,12 +831,24 @@ Happy coding!
expect(response.statusCode).toEqual(200);
});
test('PUT returns 400 status code with invalid socials setting', async () => {
test('PUT rejects non-url values', async () => {
const response = await superPut('/update-my-socials').send({
website: 'invalid',
twitter: '',
linkedin: '',
githubProfile: 'invalid'
githubProfile: ''
});
expect(response.body).toEqual(updateErrorResponse);
expect(response.statusCode).toEqual(400);
});
test('PUT only accepts urls to certain domains', async () => {
const response = await superPut('/update-my-socials').send({
website: '',
twitter: '',
linkedin: '',
githubProfile: 'https://x.com/should-be-github'
});
expect(response.body).toEqual(updateErrorResponse);
@@ -1122,3 +1138,29 @@ describe('getWaitMessage', () => {
);
});
});
describe('validateSocialUrl', () => {
it.each(['githubProfile', 'linkedin', 'twitter'] as const)(
'accepts empty strings for %s',
social => {
expect(validateSocialUrl('', social)).toBe(true);
}
);
it.each([
['githubProfile', 'https://something.com/user'],
['linkedin', 'https://www.x.com/in/username'],
['twitter', 'https://www.toomanyexes.com/username']
] as const)('rejects invalid urls for %s', (social, url) => {
expect(validateSocialUrl(url, social)).toBe(false);
});
it.each([
['githubProfile', 'https://something.github.com/user'],
['linkedin', 'https://www.linkedin.com/in/username'],
['twitter', 'https://twitter.com/username'],
['twitter', 'https://x.com/username']
] as const)('accepts valid urls for %s', (social, url) => {
expect(validateSocialUrl(url, social)).toBe(true);
});
});

View File

@@ -69,6 +69,35 @@ export const isPictureWithProtocol = (picture?: string): boolean => {
}
};
const ALLOWED_DOMAINS_MAP = {
githubProfile: ['github.com'],
linkedin: ['linkedin.com'],
twitter: ['twitter.com', 'x.com']
};
/**
* Validate a social URL.
*
* @param socialUrl The URL to check.
* @param key The key of the allowed socials and domains.
* @returns Whether the URL is valid.
*/
export const validateSocialUrl = (
socialUrl: string,
key: keyof typeof ALLOWED_DOMAINS_MAP
): boolean => {
if (!socialUrl) return true;
try {
const url = new URL(socialUrl);
const domains = ALLOWED_DOMAINS_MAP[key];
const domainAndTld = url.hostname.split('.').slice(-2).join('.');
return domains.includes(domainAndTld);
} catch {
return false;
}
};
/**
* Plugin for all endpoints related to user settings.
*
@@ -328,6 +357,18 @@ ${isLinkSentWithinLimitTTL}`
errorHandler: updateErrorHandler
},
async (req, reply) => {
const valid = (['twitter', 'githubProfile', 'linkedin'] as const).every(
key => validateSocialUrl(req.body[key], key)
);
if (!valid) {
void reply.code(400);
return reply.send({
message: 'flash.wrong-updating',
type: 'danger'
});
}
try {
await fastify.prisma.user.update({
where: { id: req.user?.id },