Hotfix: Patch JWT Exploit with RSA256

This commit is contained in:
ADAMJR 2021-10-09 20:05:19 +01:00
parent 6a52fabee1
commit 8b50edd197
16 changed files with 67 additions and 42 deletions

View File

@ -8,6 +8,18 @@ Custom Frontend and Backend that is similar to Discord.
<a href="https://ibb.co/st2q2B0"><img src="https://i.ibb.co/fQ2H2ch/Screenshot-from-2021-08-30-11-55-01.png" alt="Screenshot-from-2021-08-30-11-55-01" border="0" /></a>
<a href="https://ibb.co/SydPgTY"><img src="https://i.ibb.co/qjWd8Gq/Screenshot-from-2021-08-30-13-30-43.png" alt="Screenshot-from-2021-08-30-13-30-43" border="0" /></a>
---
## Setup
1. Clone the repo.
2. Generate SSH keys.
From app folder: `mkdir -p backend/keys && ssh-keygen -t rsa -b 2048 -m PEM -f backend/keys/accord.app`
3. Install npm packages.
From app folder: `cd frontend && npm i && cd ../backend && npm i`
---
## Features
- **Server Channels**

1
backend/.gitignore vendored
View File

@ -1,4 +1,5 @@
.env
keys/
node_modules/
lib/

View File

@ -21,6 +21,10 @@ export interface SelfUserDocument extends Document, UserTypes.Self {
changePassword: (...args) => Promise<any>;
register: (...args) => Promise<any>;
}
export interface PureUserDocument extends SelfUserDocument {
hash: string;
salt: string;
}
export const User = model<UserDocument>('user', new Schema({
_id: {

View File

@ -1,11 +1,15 @@
import DBWrapper from './db-wrapper';
import jwt from 'jsonwebtoken';
import { SelfUserDocument, User, UserDocument } from './models/user';
import { PureUserDocument, SelfUserDocument, User, UserDocument } from './models/user';
import { generateSnowflake } from './snowflake-entity';
import { APIError } from '../rest/modules/api-error';
import { GuildMember } from './models/guild-member';
import { Guild, GuildDocument } from './models/guild';
import { UpdateQuery } from 'mongoose';
import { UpdateQuery, connection } from 'mongoose';
import { promisify } from 'util';
import { readFile } from 'fs';
const readFileAsync = promisify(readFile);
export default class Users extends DBWrapper<string, UserDocument> {
public async get(id: string | undefined): Promise<UserDocument> {
@ -16,7 +20,7 @@ export default class Users extends DBWrapper<string, UserDocument> {
return this.secure(user);
}
// TODO: test that this is fully secure
// TODO: TESTME
public secure(user: UserDocument): UserDocument {
const u = user as any;
u.email = undefined;
@ -26,6 +30,13 @@ export default class Users extends DBWrapper<string, UserDocument> {
u.verified = undefined;
return u;
}
public async getPure(id: string | undefined): Promise<PureUserDocument> {
const users = connection.db.collection('users');
const user = await users.findOne({ _id: id });
if (!user)
throw new TypeError('User not found');
return user;
}
public async getSelf(id: string | undefined): Promise<SelfUserDocument> {
const user = await User.findById(id);
@ -55,21 +66,24 @@ export default class Users extends DBWrapper<string, UserDocument> {
await User.updateOne({ _id: id }, partial);
}
public createToken(userId: string, expire = true) {
public async createToken(user: SelfUserDocument, expire = true) {
// too insecure to keep in memory
const key = await readFileAsync('./keys/accord.app', { encoding: 'utf-8' });
return jwt.sign(
{ _id: userId },
'secret',
(expire) ? { expiresIn: '7d' } : {},
{ id: user.id },
key,
{ algorithm: 'RS256', expiresIn: (expire) ? '7d' : undefined },
);
}
public idFromAuth(auth: string | undefined): string {
public async idFromToken(auth: string | undefined): Promise<string> {
const token = auth?.slice('Bearer '.length);
return this.verifyToken(token);
return await this.verifyToken(token);
}
public verifyToken(token: string | undefined): string {
const decoded = jwt.verify(token as string, 'secret') as UserToken;
return decoded?._id;
public async verifyToken(token: string | undefined): Promise<string> {
// too insecure to keep in memory
const key = await readFileAsync('./keys/accord.app', { encoding: 'utf-8' });
const decoded = jwt.verify(token as string, key, { algorithms: ['RS256'] }) as UserToken;
return decoded?.id;
}
public async getUserGuilds(userId: string): Promise<GuildDocument[]> {
@ -101,4 +115,4 @@ export default class Users extends DBWrapper<string, UserDocument> {
}
}
interface UserToken { _id: string };
interface UserToken { id: string };

View File

@ -14,7 +14,7 @@ const users = Deps.get<Users>(Users);
export async function updateUser(req: Request, res: Response, next: NextFunction) {
try {
const token = req.get('Authorization') as string;
const id = users.idFromAuth(token);
const id = await users.idFromToken(token);
res.locals.user = await users.getSelf(id);
} finally {

View File

@ -33,7 +33,7 @@ router.post('/login', (req, res, next) => {
message: 'Check your email for a verification code',
});
}
res.status(201).json({ token: users.createToken(user.id) });
res.status(201).json({ token: await users.createToken(user) });
});
router.post('/register', async (req, res) => {
@ -45,7 +45,7 @@ router.post('/register', async (req, res) => {
await sendEmail.verifyEmail(user.email, user);
res.status(201).json(users.createToken(user.id));
res.status(201).json(await users.createToken(user));
});
router.get('/verify', async (req, res) => {
@ -69,7 +69,7 @@ router.get('/verify', async (req, res) => {
await user.save();
res.json({ message: 'Email verified' });
} else if (code.type === 'LOGIN')
res.json({ token: users.createToken(user.id) });
res.json({ token: await users.createToken(user) });
});
router.get('/email/forgot-password', async (req, res) => {
@ -102,6 +102,6 @@ router.post('/change-password', async (req, res) => {
return res.status(200).json({
message: 'Password changed',
token: users.createToken(user.id),
token: await users.createToken(user),
} as REST.From.Post['/auth/change-password']);
});

View File

@ -43,7 +43,7 @@ router.get('/apps/new', async (req, res) => {
username: app.name,
password: generateInvite(),
}, true)).id;
app.token = users.createToken(user.id, false);
app.token = await users.createToken(user.id, false);
await app.save();
res.json(app);
@ -60,9 +60,9 @@ router.get('/apps/:id', async (req, res) => {
router.patch('/apps/:id', async (req, res) => {
const app = await App.findById(req.params.id);
if (!app || app.ownerId !== res.locals.user.id)
return res.status(403).json({ message: 'Forbidden' });
guard.validateKeys('app', req.body);
return res.status(403).json({ message: 'Forbidden' });
// FIXME: don't use req.body
await app.update(req.body, { runValidators: true });
res.json(app);
});
@ -72,7 +72,8 @@ router.get('/apps/:id/regen-token', async (req, res) => {
if (!app || app.ownerId !== res.locals.user.id)
return res.status(403).json({ message: 'Forbidden' });
app.token = users.createToken(app.userId, false);
const pureAppUser = await users.getPure(app.userId);
app.token = await users.createToken(pureAppUser, false);
await app.save();
res.json(app.token);

View File

@ -50,7 +50,7 @@ export class REST {
this.app.use(`${this.prefix}/auth`, authRoutes);
this.app.use(`${this.prefix}/invites`, invitesRoutes);
// this.app.use(`${this.prefix}/devs`, devRoutes);
// this.app.use(`${this.prefix}/dev`, devRoutes);
this.app.use(`${this.prefix}/channels`, channelsRoutes);
this.app.use(`${this.prefix}/guilds`, guildsRoutes);
this.app.use(`${this.prefix}/users`, usersRoutes);

View File

@ -72,17 +72,10 @@ export class WSGuard {
const isAllowedByOverride = has(cumulativeAllowPerms, permInteger);
const isDeniedByOverride = has(cumulativeDenyPerms, permInteger);
console.log(permInteger); // 2048
console.log(canInherently); // true
console.log(isAllowedByOverride); // false
console.log(isDeniedByOverride); // false
console.log('can', (canInherently && !isDeniedByOverride) || isAllowedByOverride);
return (canInherently && !isDeniedByOverride) || isAllowedByOverride;
}
public async decodeKey(token: string) {
return { id: this.users.verifyToken(token) };
return { id: await this.users.verifyToken(token) };
}
}

View File

@ -36,7 +36,7 @@ describe.skip('auth-routes', () => {
}) as any;
credentials.email = user.email;
authorization = `Bearer ${users.createToken(user.id)}`;
authorization = `Bearer ${await users.createToken(user.id)}`;
});
afterEach(async () => await Mock.cleanDB());

View File

@ -25,7 +25,7 @@ describe('channel-routes', () => {
channel = guild.channels[0];
user = await users.get(guild.ownerId);
authorization = `Bearer ${users.createToken(user.id)}`;
authorization = `Bearer ${await users.createToken(user.id)}`;
});
afterEach(async () => await Mock.cleanDB());

View File

@ -27,7 +27,7 @@ describe('guilds-routes', () => {
user = await users.get(guild.ownerId);
invite = await Mock.invite(guild.id);
authorization = `Bearer ${users.createToken(user.id)}`;
authorization = `Bearer ${await users.createToken(user.id)}`;
});
afterEach(async () => await Mock.cleanDB());
@ -82,7 +82,7 @@ describe('guilds-routes', () => {
);
});
it('GET /:id/invites, is guild manager, returns all invites', async () => {
authorization = `Bearer ${users.createToken(guild.members[1].userId)}`;
authorization = `Bearer ${await users.createToken(guild.members[1].userId)}`;
const role = await Role.findById(guild.roles[0].id);
await Mock.giveRolePerms(role, PermissionTypes.General.MANAGE_GUILD);

View File

@ -25,7 +25,7 @@ describe('invite-routes', () => {
user = await users.get(guild.ownerId);
invite = await Mock.invite(guild.id);
authorization = `Bearer ${users.createToken(user.id)}`;
authorization = `Bearer ${await users.createToken(user.id)}`;
});
afterEach(async () => await Mock.cleanDB());

View File

@ -24,7 +24,7 @@ describe('ready', () => {
({ event, user, ws, guild } = await Mock.defaultSetup(client, Ready));
users = new Users();
token = users.createToken(user.id);
token = await users.createToken(user.id);
});
afterEach(async () => await Mock.afterEach(ws));
@ -102,6 +102,6 @@ describe('ready', () => {
}
async function makeOwner() {
ws.sessions.set(client.id, guild.ownerId);
token = users.createToken(user.id);
token = await users.createToken(user.id);
}
});

View File

@ -68,7 +68,7 @@ describe.only('user-update', () => {
}
async function regenToken(id = user.id) {
token = Deps
token = await Deps
.get<Users>(Users)
.createToken(id, false);
}

View File

@ -23,7 +23,7 @@ const MessageContent: FunctionComponent<MessageContentProps> = ({ message }) =>
codeLine: /`(.*?)`/gs,
blockQuoteMultiline: />>> (.*)/gs,
blockQuoteLine: /^> (.*)$/gm,
url: /http:\/\/(.*)|https:\/\//gm,
url: /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))/gm,
}
const format = (content: string) => content