Admin model
Given the custodial nature of the wallet, the team needs a way to take administrative action on user wallets. While Zellic did not identify any vulnerabilities in the audited section of the application regarding this functionality, we feel that the current implementation constitutes a single point of failure and a likely attack target for malicious actors.
Every request is routed through a middleware that verifies the JWT passed with the request.
export const AuthMiddleware = async (req, res, next) => {
const { token = {} } = req.cookies;
const { authorization } = req.headers;
const authToken = authorization?.split('Bearer ')[1];
if (!authToken && !token) return res.status(401).send({ error: 'Unauthorized' });
try {
const decodedToken = jwt.verify(
authToken || token,
CLIENT_JWT_SECRET
) as jwt.JwtPayload;
req.auth = {
userId: decodedToken.sub,
sessionId: decodedToken.jti,
isAdmin: ADMIN_EMAIL_LIST.includes(decodedToken.email),
email: decodedToken.email
};
} catch (e) {
return res
.status(401)
.send({ error: `JWT could not be verified due to ${e.message}` });
}
return next();
};
Source: packages/rest-apis/wallet-api/src/app/middlewares/AuthMiddleware.ts
Here the req.auth
object is constructed. One of the fields is the isAdmin
field, which is a simple check if the supplied user email is contained in a list of admin emails. The list contains 14 @lootrush.com employee emails.
The controllers then access this auth object and check if the isAdmin
field is true. If it is false, meaning the user is not an admin, the values from the auth object are used that have been signed in the JWT. If the user is an admin, then the parameters are pulled from the request, allowing an admin to interact with wallets not belonging to them.
class CustodialWalletMethodCallsController {
async create(req, res) {
try {
const { userId } = req?.auth?.isAdmin ? req.params : req.auth;
(...)
const wallet = await walletService.find({ userId, address: callPayload.walletAddress }, false);
if (!wallet)
return res.status(404).send();
const result = await walletService.callMethod(wallet, callPayload.method, callPayload.params);
return res.status(200).json({ ...callPayload, result });
} (..)
}
}
Source: packages/rest-apis/wallet-api/src/app/controllers/CustodialWalletMethodsController.ts
We believe this will be a primary target for attackers as it allows almost total control of the custodial system. A compromise of a single admin email could compromise the entire application.
Further, there may be vulnerabilities in the rest of the codebase that would allow an attacker to forge JWT tokens with arbitrary emails, allow account takeover issues on the website itself, or allow email parsing issues, which would all lead to the same impact of an admin account being used to compromise the entire system as a result.
A more robust solution may be found that prevents an admin account from taking direct actions on user wallets, or something like a multi-signature approach requiring multiple admin approvals to take direct actions on user wallets.