1 Executive Summary
This report presents the results of our engagement with the Consensys Customer Success team to review the MetaMask Web3 Tickets Platform Snap.
The review was conducted over 1 week, from December 11, 2023 to December 15, 2023, by Valentin Quelquejay. A total of 5 person-days were spent.
2 Scope
Our review focused on the commit hash 8c6a772812160f7816c8f833feed9591fe4a771f. The list of files in scope can be found in the Appendix.
2.1 Objectives
Together with the team, we identified the following priorities for our review:
- Correctness of the implementation, consistent with the intended functionality and without unintended edge cases.
- Identify vulnerabilities particular to the MetaMask Snaps SDK integration in coherence with the MetaMask Snap Threat Model describing a Snap as an extension of the MetaMask Wallet Trust Module.
- Adherence of the implementation with the provided specifications.
3 Snap Overview
The Web3-Tickets Snap is intended for use in coordination with the Web3-Tickets Platform dapp. The user must sign in through the dapp using the Sign-in with Ethereum flow. Then, the user can utilize the Snap to directly receive notifications from the Web3-Tickets Platform in their wallet, and perform different actions such as replying to new ticket comments.
3.1 Capabilities
- Communicate with the Metamask Web3-Tickets platform dapp
- Access the user’s address
- Execute a Cron job every second to fetch and process new ticket comments
- Display notifications & dialogs in MetaMask or natively in the browser
- GET/POST from/to external platform API(s)
- Store local data: API access tokens and blob data
3.2 Permissions
4 Findings
Each issue has an assigned severity:
- Minor issues are subjective in nature. They are typically suggestions around best practices or readability. Code maintainers should use their own judgment as to whether to address such issues.
- Medium issues are objective in nature but are not security vulnerabilities. These should be addressed unless there is a clear reason not to.
- Major issues are security vulnerabilities that may not be directly exploitable or may require certain conditions in order to be exploited. All major issues should be addressed.
- Critical issues are directly exploitable security vulnerabilities that need to be fixed.
4.1 An Attacker Can Flood the Support by Creating Many Tickets for Arbitrary Addresses Critical ✓ Fixed
Resolution
create_ticket
function has been removed from both the site and snap. The create tickets handler has also been disabled in the backend, it is not used for now. When the time comes we’ll take care of the DoS protection and authorization”.
Description
The POST endpoint called by the create_ticket
function does not implement any access control mechanism nor does not seem to implement any kind of DoS protection. An attacker can create tickets for any arbitrary address by providing the corresponding address_key
, even if he does not own the corresponding private key. That is made possible because the backend does not check whether the API key is authorized for the address key that is submitted.
Examples
packages/snap/utils/backend_functions.ts:L5-L18
export async function create_ticket(req_address: string, title: any, description: any, apikey: any) {
const address_key = SHA256(req_address, { outputLength: 32 }).toString();
try{
const requester = "[email protected]";
const ticketData = {ticket: {
subject: title,
requester: requester,
tags: 'anon_' + address_key,
comment:{body: description }
}};
const url = 'https://71z6182pq3.execute-api.eu-west-1.amazonaws.com/default/tickets?create=true';
fetch(url, {
Recommendation
When new tickets are submitted, the backend must check that the API key is authorized to create new tickets for that particular address key. Additionally, one should consider implementing proper rate limiting or equivalent DoS protection to prevent the mass creation of tickets by a single user or address, mitigating the risk of support flooding.
4.2 Broken Authorization Scheme Critical ✓ Fixed
Resolution
Description
Anyone can enumerate all tickets belonging to any wallet address by providing the corresponding address key (SHA256 of the address) to the right API endpoint. Users can also create tickets on behalf of other users by sending a POST request to the API with the corresponding address key. Finally, any user can view and post updates to tickets belonging to other users by injecting arbitrary ticket IDs in GET/POST requests sent to the API. The root cause is that the ticket IDs and address keys are not authenticated against a particular user ID/access token. Thus, it allows unauthorized access to potentially sensitive ticket information and the ability to impersonate users and create tickets and post comments to tickets belonging to other users. This is a critical flaw which must be fixed.
Examples
packages/snap/utils/backend_functions.ts:L5-L17
export async function create_ticket(req_address: string, title: any, description: any, apikey: any) {
const address_key = SHA256(req_address, { outputLength: 32 }).toString();
try{
const requester = "[email protected]";
const ticketData = {ticket: {
subject: title,
requester: requester,
tags: 'anon_' + address_key,
comment:{body: description }
}};
const url = 'https://71z6182pq3.execute-api.eu-west-1.amazonaws.com/default/tickets?create=true';
packages/snap/utils/backend_functions.ts:L38-L42
const address_key = SHA256(req_address, { outputLength: 32 }).toString();
try{
const url = 'https://71z6182pq3.execute-api.eu-west-1.amazonaws.com/default/tickets';
const final_url = url + '?address=' + address_key;
packages/snap/utils/backend_functions.ts:L67-L72
export async function get_ticket_comments(ticket_id : any, req_address: any, apikey: any){
let json_ticket_comments = null;
const address_key = SHA256(req_address, { outputLength: 32 }).toString();
try{
const url = 'https://71z6182pq3.execute-api.eu-west-1.amazonaws.com/default/tickets?ticketId=' + ticket_id;
packages/snap/utils/backend_functions.ts:L94-L100
export async function update_ticket(ticket_id: any, input_data: any, req_address: any, apikey: any) {
console.log(`Updating ticket ${ticket_id} with comment: `, input_data);
const address_key = SHA256(req_address, { outputLength: 32 }).toString();
try {
const url = 'https://71z6182pq3.execute-api.eu-west-1.amazonaws.com/default/tickets?ticketId='
+ ticket_id + '&create=false' + '&from_snap';
Recommendation
Implement strict validation and proper access control mechanisms to ensure that users can only create, view and modify tickets they are authorized to access. This could involve verifying user permissions against the ticket ID and address key before granting access or allowing modifications.
4.3 Any Website Connected to the Snap Can Access the Address and API Key and Impersonate the User Critical ✓ Fixed
Resolution
get_snap_state
RPC endpoint, and implemented origin validation in the Snap. Note that the whitelisted origin will still have to be updated to the production domain.
Description
Currently, there is no validation of the origin for RPC calls made to the Snap. Thus, any dapp connected to the Snap can call any RPC endpoint, which allows an attacker crafting a malicious dapp to access a user’s tickets and impersonate the user to support services. The vulnerability arises because the RPC call handler onRpcRequest()
can be invoked by any dapp as it does not validate the origin of the call. This enables any connected dapp to call get_snap_state
, exposing critical user information like its userID, API token, and ticket details.
Examples
packages/snap/src/index.ts:L242-L247
export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
switch (request.method) {
case 'set_snap_state':
let allTicketCommentsCount: any = [];
Recommendation
Implement strict origin validation in the onRpcRequest()
handler to ensure that only authorized and intended domains can invoke the Snap. Additionally, consider removing the get_snap_state
RPC endpoint if it is not strictly needed.
4.4 Missing Notifications in Case Multiple Tickets Are Updated Medium Partially Addressed
Resolution
7ef2853d21049986beefaec4f2405814319f7cfb
and provided the following statement: “If multiple tickets are updated, all of them are sent as a notification to the user, so they don’t miss any updates.
In the future, we should also check if there are too many updates and redirect the user to the dashboard instead of showing 5 notifications, for example. At the moment this is not a concern, since the average user statistically has about 1.1 tickets in their lifetime.”
Description
Currently, if multiple tickets are updated (have new comments), the Snap will only trigger a notification for the last updated ticket. The fetchAllTicketCommentsCount
function only triggers a notification for the last updated ticket, as the variable updatedTicketId
gets overridden in each iteration of the loop by the last updated ticket id. Consequently, updates to other tickets are not notified. This issue is problematic for users who have multiple tickets opened concurrently, as when there are multiple tickets with new comments, all except the last updated ticket go unnoticed.
Examples
packages/snap/src/index.ts:L83-L91
for(let i = 1; i < ticket_ids_and_comment_count.length; i += 3){
if(ticket_ids_and_comment_count[i] > allTicketCommentsCount[i]){
if(ticket_ids_and_comment_count[i+1] == 'agent'){
if (ticket_ids_and_comment_count[i - 1] == allTicketCommentsCount[i - 1]) {
fireAlerts = true;
updatedTicketId = ticket_ids_and_comment_count[i - 1];
console.log("There is an update on ticket ID: ", updatedTicketId);
console.log('Firing notifications...');
setSnapState(apiKey, address, allTicketCommentsCount, dialog);
Recommendation
Revise the notification logic to ensure that the user is notified in case multiple tickets are updated. One could accumulate all updated ticket IDs in an array and then iterate over this array to fire alerts for each updated ticket. In case there are many updated tickets, one should probably notify the user and redirect him to the dapp to read the new messages.
4.5 General Design and Performance Considerations: Data Fetching/Caching and State Update Medium Partially Addressed
Resolution
7ef2853d21049986beefaec4f2405814319f7cfb
and provided the following statement: “All of the user’s tickets (list of ticket ids and other data, not whole tickets) and only the tickets that have had updates are fetched every 30 seconds. We determine if a ticket has been updated if the id of the last comment of a ticket has changed.”.
Description
The Snap’s current design, which pulls all user tickets and comments from the server every second, poses significant performance and efficiency issues. This constant fetching process occurs for every user who has installed the Snap, leading to concerns, particularly for users with numerous tickets and comments. The Snaps has to re-process the same data repeatedly, despite having a local cache. This approach not only renders the local caching inefficient, as data is re-fetched in every cycle, but also places an unnecessary load on the backend and results in the same data being transmitted many times.
Additionally, it can also be highlighted that the snaps contain unnecessary state updates: every key in the Snap’s state is updated every time, even if it hasn’t changed. For instance, but not exclusively, here:
packages/snap/src/index.ts:L91
setSnapState(apiKey, address, allTicketCommentsCount, dialog);
packages/snap/src/index.ts:L103
setSnapState(apiKey, address, allTicketCommentsCount, dialog);
Recommendation
We recommend redesigning the data fetching mechanism to enhance efficiency and performance. One should aim to fetch only new or updated comments, potentially by cleverly leveraging ticket and comment IDs/timestamps. This would reduce the load on both the client and the server and minimize unneeded computing and data transfer. If tickets can be updated, one also needs to consider this case. In general, one should also avoid unnecessary Snap state updates by updating only the modified keys.
4.6 Markdown Injection in Snaps UI Components Medium Acknowledged
Resolution
Description
Snaps UI components are prone to markdown and control character injections. In this case, one of the risk is the potential for operators to mislead users. For example, an operator could create a comment that falsely appears as if another person has joined the discussion. This is achieved by formatting a comment with a reply followed by a separator and then falsely attributing a message to someone else, as in <officialReply>\n\n______________________\n\n<OtherPerson>: <OtherComment>
.
Considering users should only be able to see their own and the operator’s comments, and operators are assumed non-malicious, this vulnerability should not pose significant security concerns. However, one should keep that in mind in case security assumptions evolve.
Examples
packages/snap/src/index.ts:L120-L123
for (let i = 0; i < json.length; i++){
const comment = json[i]['body'];
const sender = json[i]['via']['channel'] == 'api' ? '**You**' : '**Agent**'
formatted_comments += `${sender}: ${comment}\n\n______________________\n\n`;
Recommendation
As a mitigation, one could implement comprehensive content sanitization and strict validation for markdown rendered in UI components to prevent such misleading representations. Alternatively, one could investigate rendering comments in distinct UI components.
4.7 fetchAllTicketCommentsCount()
Is Unoptimized, Overly Complex, and Lacks Untrusted Input Validation Medium ✓ Fixed
Resolution
The client addressed this issue in commit 7ef2853d21049986beefaec4f2405814319f7cfb
. Additionally, they provided the following statement:
“The fetchAllTicketCommentsCount()
method has been deleted and rebuilt from scratch. Its new name is checkTicketUpdates
and the following improvements were added:
- Improved performance (see 4.5 for more details)
- Improved error handling
- Early exit if some state variables are not available
- Reduced the number of ‘then’ statements to reduce complexion and improve readability
- Renamed function and variables for better readability”
Description
The current fetchAllTicketCommentsCount()
function is overly complex and poorly optimized. For instance, the function unnecessarily exhibits a sequence of “then” promises, which could be avoided. Moreover, the function fetches ticket comments sequentially, whereas a parallel approach would be more efficient. It also parses ticket_count
from the received JSON data and uses it to iterate over that data, yet it fails to validate that ticket_count
is less than the length of the received data. These examples illustrate that it would be a good idea to refactor and optimize the function to increase the readability and robustness of its code.
We would suggest optimizing the function and simplifying its code to improve readability and robustness. More precisely, one could make the following changes:
- Exit early if variables like
address
orapiKey
are not initialized - Sanitize and validate relevant untrusted values, such as the
ticket_count
- Refactor the data processing logic to be more succinct and readable. Consider using different data structures (e.g., objects) to simplify the logic and improve the robustness of the code.
- Leverage parallel data fetching by utilizing
Promise.all()
to concurrently fetch ticket comments - Streamline the code by removing unnecessary chains of promises, logging, and verbose comments
- Add proper error handling
packages/snap/src/index.ts:L35-L42
const fetchAllTicketCommentsCount = async () => {
let allTicketCommentsCount : any = [];
let fireAlerts = false;
let updatedTicketId = null;
const state = await getSnapState();
const address = state?.address as string
packages/snap/src/index.ts:L53-L77
await get_user_tickets(address, apiKey)
.then( async (json : any) => {
const ticket_count = json['count'];
if (ticket_count > 0)
for(let i = 0; i < ticket_count; i++){
const ticket_id = json['rows'][i]['ticket']['id'];
await get_ticket_comments(ticket_id, address, apiKey).then( (json : any) => {
if(json.length > 0){
const last_comment_source = json[json.length-1]['via']['channel'];
ticket_ids_and_comment_count.push(ticket_id);
ticket_ids_and_comment_count.push(json.length);
if(last_comment_source == 'api')
ticket_ids_and_comment_count.push('user');
else if (last_comment_source == 'web')
ticket_ids_and_comment_count.push('agent');
}
})
}
})
.then(() => {
// console.log(ticket_ids_and_comment_count);
// console.log(allTicketCommentsCount);
if(allTicketCommentsCount.length == 0){
console.log('Notification process started.');
}
packages/snap/src/index.ts:L86-L99
if (ticket_ids_and_comment_count[i - 1] == allTicketCommentsCount[i - 1]) {
fireAlerts = true;
updatedTicketId = ticket_ids_and_comment_count[i - 1];
console.log("There is an update on ticket ID: ", updatedTicketId);
console.log('Firing notifications...');
setSnapState(apiKey, address, allTicketCommentsCount, dialog);
}
}
}
}
}
}
}).then(() =>{
// console.log(ticket_ids_and_comment_count);
Recommendation
We recommend completely refactoring the fetchAllTicketCommentsCount()
function in line with these suggestions. This refactoring will not only make the code more readable and maintainable but also improve its performance.
As a future improvement, one should also investigate more optimized mechanisms to fetch tickets, such as server-side push instead of client-side pull or smarter algorithms to fetch only new comments. This would significantly improve user performance with many tickets/comments and reduce both server and client-side load. (see issue 4.5 )
4.8 Improper Paramater Sanitization in fetchAllTicketCommentsCount()
, updateTicket()
, and parseTicketComments()
Medium ✓ Fixed
Resolution
7ef2853d21049986beefaec4f2405814319f7cfb
: “All of the methods now have an early if statement causing them to exit if the needed variables are not present at the moment of their calling.”
Description
Currently, if the Snap’s state is empty or undefined, fetchAllTicketCommentsCount()
tries to fetch tickets for the key corresponding to the hash of the empty string ""
. This occurs when the snap is installed and the state has not yet been initialized by the dapp, for instance. This leads to unnecessary processing on the server and could also result in more severe issues. In general, functions should validate and sanitize any input parameters, especially if they are not trusted.
In this case, fetchAllTicketCommentsCount()
, updateTicket()
, and parseTicketComments()
should exit early if any of their parameters or values retrieved from the state, such as apiKey
, address
, or ticketID
are not defined.
Examples
packages/snap/src/index.ts:L35-L44
const fetchAllTicketCommentsCount = async () => {
let allTicketCommentsCount : any = [];
let fireAlerts = false;
let updatedTicketId = null;
const state = await getSnapState();
const address = state?.address as string
const apiKey = state?.apiKey as string
const dialog = state?.dialog as string
packages/snap/src/index.ts:L133-L139
async function updateTicket(ticketId: any, user_comment: any) {
// need a function here as getSnapState() doesn't seem to work in cron
const state = await getSnapState();
const address = state?.address as string
const apiKey = state?.apiKey as string
const update_result = await update_ticket(ticketId, user_comment, address, apiKey);
packages/snap/src/index.ts:L113-L117
async function parseTicketComments(ticketId: any) {
const state = await getSnapState();
const address = state?.address as string
const apiKey = state?.apiKey as string
let formatted_comments = `Login to https://web3tickets.metamask.io to see your personal dashboard with all tickets open for your ethereum account address. \n\n`;
Recommendation
Modify fetchAllTicketCommentsCount()
, updateTicket()
, and parseTicketComments()
to include early exit conditions. These functions should immediately return or exit if essential parameters such as apiKey
, address
, or ticketID
are not defined. Implementing these checks will prevent the functions from executing with incomplete or invalid data.
4.9 Unused Imports, Inconsistent Coding Style and Unsafe Patterns Minor Acknowledged
Resolution
Description
The codebase currently exhibits a lack of consistency in coding style: inconsistent use of semi-colons, inconsistent line lengths, and other stylistic discrepancies. These inconsistencies make the codebase harder to read and maintain, potentially leading to future issues. Similarly, some typescript annotations are tautologies and should be simplified. Unneeded return statements should also be ditched.
The code also exhibits patterns that are generally considered unsafe, e.g., the use of an “if” without brackets, or loose equality (==
) instead of strict equality (===
). These patterns should be removed in favor of safer patterns.
Additionally, the codebase contains unused imports, resulting in unnecessary complexity. It also appears to transform the bundle (in snaps.config.js.bundlerCustomizer()
) by appending the Buffer package, which does not seem to be used in the Snap.
Finally, for conciseness and clarity, one should remove unneeded code and statements, such as unreachable code.
Example
Inconsistent use of semi-colons:
packages/snap/src/index.ts:L135-L137
const state = await getSnapState();
const address = state?.address as string
const apiKey = state?.apiKey as string
The typescript annotation allTicketCommentsCount: any | []
can be simplified to any
:
packages/snap/src/index.ts:L16-L16
const setSnapState = async (apiKey: string | null, address: string | null, allTicketCommentsCount: any | [], dialog : string | null) => {
Unsafe if
statement:
packages/snap/src/index.ts:L56-L57
if (ticket_count > 0)
for(let i = 0; i < ticket_count; i++){
Weak equality checks:
packages/snap/src/index.ts:L75-L76
if(allTicketCommentsCount.length == 0){
console.log('Notification process started.');
Unreachable code:
packages/snap/src/index.ts:L229-L230
throw new Error('Method not found.');
break;
This always return true
or throws an error. The return statement can safely be removed:
packages/snap/src/index.ts:L259-L260
await setSnapState(request.params.apiKey, request.params.address, allTicketCommentsCount, request.params.dialog);
return true;
Some extra imports:
packages/snap/src/index.ts:L2-L3
import { panel, text, heading, divider } from '@metamask/snaps-ui';
import { create_ticket, get_user_tickets, get_ticket_comments, update_ticket } from '../utils/backend_functions';
Note that these are just a few examples but more instances of some of these patterns are present in the codebase.
Recommendation
Implement the use of a linter to enforce a consistent coding style throughout the codebase. This will not only improve the readability and maintainability of the code but also help to prevent potential bugs that can arise from stylistic inconsistencies. Refactor unsafe code patterns, particularly ensuring that ‘if’ statements and similar constructs are used with brackets to prevent potential bugs. Remove any unused imports and unneeded code segments, such as unreachable code, to enhance the codebase’s clarity, safety, and reduce bundle size.
4.10 Lack of Inline Code Documentation Minor Acknowledged
Resolution
Description
The codebase lacks inline documentation. This reduces the maintainability of the codebase as it can make it challenging for new developers and auditors to understand the code as it grows. Proper inline documentation is essential for maintaining code clarity and future maintainability.
Recommendation
We recommend adding inline documentation throughout the codebase. This should include comments explaining the purpose and functionality of key functions. The documentation should be clear, concise, and consistent to ensure it is helpful and understandable for anyone reviewing or working with the code.
4.11 Inconsistent Naming Conventions and Misleading Function/Variable Names Minor Partially Addressed
Resolution
Description
The codebase exhibits inconsistent use of naming conventions, alternating between camel case and underscore naming. In general, it is better to avoid mixing conventions to increase the code’s readability and maintainability. Additionally, there are instances where function and variable names are misleading, which can cause confusion about their purpose and functionality. For example, a function named fetchAllTicketCommentsCount
misleadingly suggests it returns a count, while it actually returns a boolean and a ticket ID.
Examples
packages/snap/src/index.ts:L35-L36
const fetchAllTicketCommentsCount = async () => {
packages/snap/src/index.ts:L52
let ticket_ids_and_comment_count: any[] = [];
packages/snap/src/index.ts:L37
let allTicketCommentsCount : any = [];
Recommendation
Standardize the naming conventions, preferably choosing camel case according to Javascript standard practices. Additionally, review and rename functions and variables to accurately reflect their purpose and output. This will enhance code clarity and significantly improve the auditability and maintainability of the codebase.
Appendix 1 - Files in Scope
This audit covered the following files:
File | SHA-1 hash |
---|---|
packages/snap/snap.config.js | d809056221c5c0725b9708c84c979af743522a24 |
packages/snap/src/index.ts | 86921d84ef0053ee2f561e912d56e058bca97186 |
packages/snap/utils/backend_functions.ts | 2e7327102492531699b3627a7174f150adf66c3c |
Appendix 2 - Disclosure
Consensys Diligence (“CD”) typically receives compensation from one or more clients (the “Clients”) for performing the analysis contained in these reports (the “Reports”). The Reports may be distributed through other means, including via Consensys publications and other distributions.
The Reports are not an endorsement or indictment of any particular project or team, and the Reports do not guarantee the security of any particular project. This Report does not consider, and should not be interpreted as considering or having any bearing on, the potential economics of a token, token sale or any other product, service or other asset. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. No Report provides any warranty or representation to any third party in any respect, including regarding the bug-free nature of code, the business model or proprietors of any such business model, and the legal compliance of any such business. No third party should rely on the Reports in any way, including for the purpose of making any decisions to buy or sell any token, product, service or other asset. Specifically, for the avoidance of doubt, this Report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. CD owes no duty to any third party by virtue of publishing these Reports.
A.2.1 Purpose of Reports
The Reports and the analysis described therein are created solely for Clients and published with their consent. The scope of our review is limited to a review of code and only the code we note as being within the scope of our review within this report. Any Solidity code itself presents unique and unquantifiable risks as the Solidity language itself remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond specified code that could present security risks. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. In some instances, we may perform penetration testing or infrastructure assessments depending on the scope of the particular engagement.
CD makes the Reports available to parties other than the Clients (i.e., “third parties”) on its website. CD hopes that by making these analyses publicly available, it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.
A.2.2 Links to Other Web Sites from This Web Site
You may, through hypertext or other computer links, gain access to web sites operated by persons other than Consensys and CD. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such web sites’ owners. You agree that Consensys and CD are not responsible for the content or operation of such Web sites, and that Consensys and CD shall have no liability to you or any other person or entity for the use of third party Web sites. Except as described below, a hyperlink from this web Site to another web site does not imply or mean that Consensys and CD endorses the content on that Web site or the operator or operations of that site. You are solely responsible for determining the extent to which you may use any content at any other web sites to which you link from the Reports. Consensys and CD assumes no responsibility for the use of third-party software on the Web Site and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.
A.2.3 Timeliness of Content
The content contained in the Reports is current as of the date appearing on the Report and is subject to change without notice unless indicated otherwise, by Consensys and CD.