Leviathan Security Group - Penetration Testing, Security Assessment, Risk Advisory

View Original

WebSockets and Meteor: A Penetration Tester’s Guide to Meteor

A Meteor crash course

Meteor is a web application framework that uses JavaScript for both client- and server-side code and stores its server-side data in MongoDB. Temporary client-side data storage is provided through Minimongo, an in-memory browser data store that implements the MongoDB interface. Meteor has a built-in package management tool that streamlines the use of packages published with npm or with Atmosphere, a repository dedicated to Meteor packages. 

Meteor makes an ideal case study for WebSocket security because the only HTTP requests Meteor sends are downloads of media files and client-side JavaScript bundles. The client can only meaningfully interact with the server over WebSockets. Therefore, an effective pen test on a Meteor app requires the tester to craft their attack traffic using Meteor’s WebSocket communication protocol, the Distributed Data Protocol (DDP). 

Methods and publications 

Two types of functionality are exposed to the client through DDP: methods and publications. A method is a named action that the client can perform on the server, with optional input parameters and returned data. Create, delete, and update operations are performed through method calls. A publication is a logical group of data records, usually documents stored in Mongo, to which a client can subscribe. When a subscription is first opened, the server sends the client each record in the publication. The subscription will remain open until the client or server manually terminates it, or the user closes the browser window. While the subscription is open, the server automatically sends notifications to the client about records that have been added, removed, updated, or reordered. 

A few examples will illustrate how developers usually implement this functionality.  Typical server-side code for creating a method looks like this: 

Meteor.methods('movies.insert', (movie) => { 
  if (!this.userId) { 
    throw new Meteor.Error(‘You must log in first.’); 
  } 
  check(movie.title, String); 
  check(movie.genre, String); 
  MoviesCollection.insert({ 
    'title': movie.title, 
    'genre': movie.genre, 
    'ownerId': this.userId 
  }); 
};

The check function validates user input and is often utilized to validate the input’s data type. If a check fails, the server will generate an error message with the text Match failed under default conditions. 

A publication is typically created by writing a function that returns a MongoDB cursor: 

Meteor.publish('nowShowing', function publishMovies() { 
  return MoviesCollection.find({}, {sort:{title: 1}}); 
});

There are other tools for constructing publications, such as the publish-composite package and the low-level publication management API

Distributed Data Protocol 

Meteor’s network communications follow the Distributed Data Protocol (DDP), which is designed around publications and methods. Each DDP message is sent in a JSON-based format with a msg field that identifies the message’s type. Client-sent messages calling a method or subscribing to a publication also include a params field containing an array of parameters. 

When a new connection is opened, the client must begin with a connect message and wait for a connected reply before continuing. A client logs in with a call to the login method. If authentication is successful, the server sends a response containing a token field that behaves like a session token. During subsequent DDP connections, clients can use that token to log back in with a call to the loginToken method. 

The following example shows  the DDP messages from a short session (client-sent messages are shown in green; server-sent messages are show in orange): 

{"msg":"connect","version":"1","support":["1","pre2","pre1"]}	 
{"msg":"connected","session":"Y4PufyofdCkPp9G62"}
{"msg":"method","id":"1","method":"login","params":[{"user":{"username":"admin"},"password":{"digest":"5723360ef11043a879520412e9ad897e0ebcb99cc820ec363bfecc9d751a1a99","algorithm":"sha-256"}}]}  
{"msg":"updated","methods":["1"]} {"msg":"result","id":"1","result":{"id":"PowDZLy5jGoeazfo9","token":"...","tokenExpires":{"$date":1668014031493},"type":"password"}} {"msg":"sub","id":"HAPxTBTbDQczWoMBm","name":"nowShowing","params":[]} 
{"msg":"added","collection":"movies","id":"DeAAuxnxmqccSDBAW","fields":{"title":"Sunset Boulevard"}} {"msg":"added","collection":"movies","id":"Bj7PJyZRRBG57hPxG","fields":{"title":"Citizen Kane"}} {"msg":"ready","subs":["HAPxTBTbDQczWoMBm"]} {"msg":"ping"} 
{"msg":"pong"} 
{"msg":"added","collection":"movies","id":"xTMCtsaqzEEiS7KnK","fields":{"title":"Casablanca"}} {"msg":"method","id":"2","method":"movies.insert","params":[{"title":"Rebel Without a Cause"}]} 
{"msg":"result","id":"2"} {"msg":"added","collection":"movies","id":"dBMNNHRWxdwbRa3vy","fields":{"title":"Rebel Without a Cause"}} {"msg":"updated","methods":["2"]}

Each client-sent message contains an id field that is used when a server-sent message needs to reference a specific client-sent message. For example, error messages will contain the id of the original message in the offendingMessage field. Responses to method calls and the ready messages, which tell the client a subscription is fully populated, have an id field matching that of the original method call or subscription message. The ping and pong messages shown above are Meteor’s heartbeat messages that replicate the functionality of WebSocket’s ping and pong control frames

Method calls and subscriptions have another important difference. A method call produces a response encapsulated in a single message containing either an error or a result.[1]  A subscription, however, produces a series of messages in response, and the server commits to sending updates as long as the subscription remains open. Method calls are one-shot exchanges, but subscriptions are long-term stateful operations whose output consists of multiple messages. It is also worth noting that the name of a publication is not necessarily the same as the name of the collection that the server populates through that publication. In the example above, the publication nowShowing contained documents in the collection movies. Publications can even include documents from multiple collections. This difference will need to be accounted for in any security tools designed for Meteor apps.

User enumeration 

First, Meteor’s password-based authentication system, which is implemented in the accounts-password package, has a user enumeration vulnerability. If a user provides an incorrect password for an account that exists, the error message reads Incorrect password. If the provided username is invalid, the error message reads, User not found. Admittedly, this vulnerability is sometimes unavoidable in applications with social features, but if there are no such social features, user IDs should never be enumerable through a login or registration page. Moreover, Meteor lets users log in with either a username or email address, and the error messages are the same in either case. That means attackers can enumerate either usernames or email addresses. 

Ineffective rate limiting 

Second, the rate limiting applied to login attempts is trivial to bypass. By default, users are limited to five calls to any of the login, user registration, and password reset methods in a ten-second period (see accounts-base/accounts_server.js in Meteor’s source code). However, this rate limit only applies to a single DDP connection. Multiple connections opened at the same time from the same IP address have their own independent rate limits. The simplest way to exploit this behavior when brute-forcing accounts is to open a DDP connection, send five login attempts with no delay, wait for the responses, then drop the DDP connection and open a new one. Since there is no enforced limit on concurrent connections, the client can run as many parallel threads as the server can handle. 

Information disclosure risks 

Third, Meteor leans heavily into isomorphic JavaScript, also known as universal JavaScript, in a way that can lead to accidental leakage of server-side code. Sometimes identical code, such as calls to the Mongo.Collection API, can do two different things when run on the server and on the client. That can be confusing in and of itself, but a bigger issue is the somewhat convoluted set of rules governing which files are included in the client-side bundle and the order in which they are loaded. Careless code organization practices can lead to leakage of server-side code, or even disclosure of secrets.  

No authorization controls by default 

Fourth, by default, Meteor has its autopublish and insecure packages enabled, which are handy for rapid prototyping but unacceptable for production deployments. autopublish ensures that all collections are automatically published to every client, and insecure gives all clients full write access to all Mongo collections. Although the documentation advises disabling these packages prior to production deployment, if a developer forgets, major access control violations will occur. 

Long authentication token lifetimes 

Fifth, the default lifetimes for various authentication-related tokens are atypically high. Recall that login tokens behave like session tokens and allow a user to log back in with a call to the loginToken method. These tokens are valid for a whopping 90 days, so theft of one of these tokens will give an attacker long-term access. Password reset tokens are valid for three days, and the email confirmation tokens generated upon account creation last for 30 days. These last two lifetimes leave a wide window for attackers to use tokens stolen from a user’s inbox[2].

In addition to these weaknesses, NoSQL injection attacks may be possible since Meteor uses MongoDB for its main data store. SQL injection is also a concern because it is possible to connect a Meteor app to a relational database. 

Consider the following example, where regular users see posts from groups of which they are members, but admin users can see posts in all groups: 

//Don’t do this!  The returned cursor does not see changes to the isAdmin or groupMemberships fields 
Meteor.publish('myPosts', function publishMyPosts() { 
  const u = Meteor.user(); 
  if (u.isAdmin) { //Security decision irrevocably made here 
    return PostsCollection.find({}); 
  } 
  const myGroupIds = u.groupMemberships; 
  return PostsCollection.find({ 
    groupId: { 
      $in: groupIds 
    } 
  }); 
});

This publication will contain the correct records when it is initialized, but what happens if the user’s isAdmin flag is deactivated while a subscription is active? When a publication returns a cursor, Meteor monitors that cursor for changes to the result set and sends updates to the client. However, in this example, the cursor’s result set stays the same. A security decision made at the beginning of the publication function needs to be reevaluated, but Meteor has no way of knowing that. If the user’s privileges are reduced through the isAdmin or groupMembership fields while a myPosts subscription is active, the publication cursor will become stale, and the user’s browser will receive copies of posts the user is not authorized to see. 

There are two ways to avoid this vulnerability. First, if the access control rules are simple enough, it may be possible to implement the authorization and data retrieval logic in one query using MongoDB aggregation operations. Second, the Meteor documentation recommends using the publish-composite package to write publications with multiple cursors that are run in stages. If one cursor changes, the data set will be rebuilt appropriately. 

Most applications of substantial size will have a sophisticated access control system that will need to examine multiple data records to make authorization decisions. In any engagement where source code is provided, pen testers should look for this antipattern. 

Conclusion 

Meteor is a sophisticated and powerful framework, but it has its share of pitfalls. There are multiple vulnerabilities in Meteor’s core that give attackers much to work with if developers have not changed the system’s default behavior. 

The final part of this series will introduce a Meteor pen testing tool that exploits some of these vulnerabilities and demonstrates the principles of pen testing WebSocket applications discussed in the previous post. 

Footnotes

1. As in the example session above, the server will also send an updated message when it “has finished sending the client all relevant data messages based on this procedure call,” but this message can be sent before or after the result message (see https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md#procedure-3). In isolation, the phrase “all relevant data messages” suggests DDP should support sending multiple result messages for a single method call, but that is not the case. The protocol specification and the Meteor guide agree that the server will send only one result or error per method call. Therefore, the updated message should be safe to ignore.

2. All authentication tokens, including login tokens, password reset tokens, and email confirmation tokens, have 256 bits of entropy, making brute-force attacks impractical. 

Credits

This article was written by former Leviathan employee Cliff Smith. You can get in touch with Cliff on his LinkedIn profile.