Hotfix: Patch JWT Exploit with RSA256
This commit is contained in:
parent
6a52fabee1
commit
8b50edd197
12
README.md
12
README.md
@ -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
1
backend/.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.env
|
||||
|
||||
keys/
|
||||
node_modules/
|
||||
lib/
|
@ -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: {
|
||||
|
@ -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 };
|
||||
|
@ -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 {
|
||||
|
@ -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']);
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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) };
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user