22

I'm building an API with websocket that serializes data through JSON. The App itself is a chat application. I came up with the following structure to send my data:

{date: '2020-05-31', time: '14:28:05', text: "Hey!", to: '<id:int>', from: '<id:int>'}

The user basically sends a message through the browser and this is received in a websocket server. The from: 'id' would be from the user sending the data whereas the to: 'id' would be to the user the data is being sent.

Looking at this I have a very bad feeling. My thoughts; The user using the App would in theory authenticate and that's where he would get his id. Then the receiver would have another id, such that is not the same as the authenticated one (obviously). The server would then look for that id and send the message but I'm not sure if this is secure.

I have some aspects that I think must be dealt correctly to protect the app from any attacker:

  • What if the attacker decides to tamper the "from:id" such that it could send arbitrary messages to anyone from any user?
  • What if the attacker builds a script that spams millions of messages by taking advantage of the "to:id" field?

Is it possible there is another security issue that I'm not concerned of?

VladiC4T
  • 379
  • 4
  • 8
  • 8
    Please don't use numerical IDs. Especially not if consecutive. You should not build a system where the users can simply be enumerated. Use random strings instead. If I'm user 55, who's user 54? If I'm user abfkg-4remq, then most likely abfkg-4remr won't even be a user, so as an attacker, I can't easily identify all user IDs – stefan Jun 01 '20 at 06:22
  • 15
    @stefan this is only relevant if user enumeration is a threat. In a lot of scenarios it's not. – vidarlo Jun 01 '20 at 09:47
  • 8
    @vidarlo maybe it's my limited imagination, but I can't see scenarios where user enumeration from the outside is a good thing. Sure, internally you have a db with all the users and that might be necessary to use, but externally, I would argue that it's almost always a flaw. – stefan Jun 01 '20 at 13:08
  • 4
    Not security related, but use the ISO format for the timestamp (or epoch). This will save a lot of headache later. Also, use a good, complete library to manipulate this time (for instance the native Python handing of time is horrendous (beurk), fortunately there is arrow, pendulum, dolorean) – WoJ Jun 01 '20 at 21:07
  • 1
    To add to previous comments, another reason not to use numeric ids is that numbers in JS are doubles, thus imprecise for big values. – Askar Kalykov Jun 02 '20 at 07:00
  • 1
    @AskarKalykov Given that you aren't doing math on IDs inside your application, (I don't know why you would,) that imprecision is very unlikely to come up in practice. You would have almost 2^53 ids available, and that's just the positive integer ones. – Ryan1729 Jun 02 '20 at 15:50
  • 1
    @Ryan1729 you won't do any math, but you almost certainly want to pass ids around, and would want to pass proper ids. It would be not so great if clients would start receiving 404 or 403 for legitimate requests just because backend devs decided to use offsets for identificators and precision of these ids as doubles fell above integer numbers. – Askar Kalykov Jun 02 '20 at 16:20
  • @AskarKalykov 2^53 is over 9 *quadrillion*. You would have more than enough distinguishable IDs for anything reasonable. You just don't assign any users any IDs outside the integer-safe range. So I'm not sure what you mean by "precision of these ids as doubles fell above integer numbers". You mean the doubles are more precise than needed? Are you worried about bugs arising from fractional doubles appearing somehow? – Ryan1729 Jun 02 '20 at 19:18
  • If we were talking about single precision floats then sure, having only around 16 million possible IDs makes assigning them non-sequentially difficult, and theoretically you might run out, but that isn't an issue with doubles specifically. – Ryan1729 Jun 02 '20 at 19:21
  • 1
    @Ryan1729 please look at the following fiddle to understand what I am talking about: https://jsfiddle.net/4ez675uf/ If you are going to receive json from backend, you wouldn't even know there was a problem. – Askar Kalykov Jun 02 '20 at 20:41
  • @AskarKalykov As I mentioned before, the point at which that starts happening is after 9 quadrillion. Specifically at 9,007,199,254,740,992. See https://jsfiddle.net/koe3fsh1/ So, just never assign a user an id that is not between 0 and 9007199254740991, and you can do a dead simple server-side check to see if the id is within that range and reject anything outside of it. (Make sure you handle NaNs correctly though.) Then you can drop any fractional part and use that as the ID, since a user ID should not be an access key. – Ryan1729 Jun 02 '20 at 20:58
  • Single quotes are not JSON. They are accepted by many parsers, but it's not part of the standard. Date/time fields (if they relate to the same entity), should be in ISO8601 format (yy-MM-ddThh:mm:ss.fffZ) which means it's easy to create/parse with standard libraries. – Neil Jun 03 '20 at 10:29

8 Answers8

51

What if the attacker decides to tamper the "from:id" such that it could send arbitrary messages to anyone from any user?

Create a session, and use the session identifier as identifier, not the user ID directly. E.g. let user send credentials, and upon successful validation, return a (short lived) session handle, that can be used in future messages.

Validate that the session exists and is active, and map it back to user server-side.

What if the attacker builds a script that spams millions of messages by taking advantage of the "to:id" field?

Rate limit users server side. For instance, disallow sending messages to more than ten different users a minute. This will probably not bother legitimate users, but will hamper spammers efforts. Tuning of the limit may obviously be needed - and it may be an idea to raise it for trusted users, based on behavior, and lower it upon receiving reports about spam from users.

vidarlo
  • 12,850
  • 2
  • 35
  • 47
  • 1
    Sounds Awesome. I think another strategy with respect to limit who can send messages to who is make the user accept some sort of invitation request before allowing the sender to send any messages just like Skype does (for example). – VladiC4T May 31 '20 at 19:51
  • 1
    Sure. Or have an initial limit of 1-2 messages for new contacts, and increase it as users reply to each other. – vidarlo May 31 '20 at 21:14
  • 3
    sockets are a constant connection, there's no need to identify the sender each message because only the sender can send on that connection anyway. Once the user id is verified, there's no need for a secondary ID/handle. – dandavis Jun 01 '20 at 04:26
  • 2
    @dandavis that's true, but with mobile browsers it seems that [there might be some problems with that approach](https://github.com/primus/primus/issues/350). Thus, some form of session may not be a bad idea. – vidarlo Jun 01 '20 at 09:46
  • The attacker will just make 100000 accounts, each of which sends 10 messages a minute. – user253751 Jun 01 '20 at 13:10
  • 1
    @user253751 so you limit account creation per source IP/range/location/in general to a reasonable threshold - and optionally add a captcha in the creation process. If your app model is fine with this, alternatively (or in addition) fall back to use shared login services (Facebook) or require a phone number. For a small service not necessary to have all from the start, but it might need to grow its defensive capabilities as its popularity grows. – Frank Hopkins Jun 01 '20 at 13:53
13

Basically, you have to treat every input from the user as potentially malicious.

Vidarlo has already mentioned two security issues and how they can be prevented in his answer.

I'd also add that the content itself ("text:") could contain malicious code (e.g. javascript snippets). Make sure that this code is not executed on the receiving end.

And I'd also check if the time seems correct. Depending on your application, having verified timestamps could be useful or even necessary.

Lukas
  • 3,138
  • 1
  • 15
  • 20
  • Oh yeah Time. In my case it would simply serve for the interface layer, nothing really fancy. Do you happen to know any blog/resources on time exploitation? I'm interested since you look to know some issues that could arrive from time not being handled correctly. – VladiC4T May 31 '20 at 19:58
  • 1
    One thing I'm thinking of is submission deadlines, for example for procurement. If a malicious sender can alter the timestamp of the message, it could look like the message was sent before the deadline (although it was not). As I wrote, this may or may not be an issue for you. – Lukas May 31 '20 at 20:10
  • 5
    take out the timestamps, you don't need them taking up message space, they can be spoofed, you know when they are sent anyway. – dandavis Jun 01 '20 at 04:31
  • 1
    @dandavis they might be useful for performance analysis of normal usage - if there is no other way on the communication channel OP uses. But yeah, no good to rely on them for anything security crucial. – Frank Hopkins Jun 01 '20 at 13:57
12

What if the attacker decides to tamper the "from:id" such that it could send arbitrary messages to anyone from any user?

Do not use from:id in your API. You already know it from user authenticated session instead and have zero reason for user to transmit it to you in the first place. And if there's nothing to transmit, there's nothing to tamper.

On that note, throw away date and time too. You already know when you've received message and don't need user to tell you that. You only'd need those if your application+API have some concept of offline/scheduled/backlog messages.

What if the attacker builds a script that spams millions of messages by taking advantage of the "to:id" field?

That's pretty old, even classic problem that have different, just as old solutions. One of the simplest is to introduce a timeout: backend remembers when the use sent a message and he can't send anything until some period passed. Some more complex solution still boil down to limiting user to some amount of messages per some period of time, but use progressively larger delays that fall off with time as more messages are sent in. Search for "throttling" or "rate limit" for some examples and ideas.

Oleg V. Volkov
  • 799
  • 5
  • 11
  • I see that takes advantage of Websocket being stateful but my Message-Dispatcher needs to set some sender id for the receiver to know to whom belongs that message. By making rid of the id how would you suggest for my Message-Dispatcher to know the message's origin? – VladiC4T Jun 01 '20 at 20:58
  • I guess I would have to rely on some Session-Manager to map the session sent from the websocket client to a user in some database (like redis)? – VladiC4T Jun 01 '20 at 21:05
  • 2
    @VladiC4T Call "authenticate" API method before sending messages and use this session. You need it anyway to understand if user is even allowed to use API and who he is allowed to message. – Oleg V. Volkov Jun 02 '20 at 00:17
  • To confirm; my "authenticate" API would check for credentials and then map an authentication identifier to the user. Then, for each message sent from the client my websocket server would grab that authentication identifier and point to the user? – VladiC4T Jun 02 '20 at 15:14
  • Ok got it. I would simply track the websocket client connection previously established by my authenticator hence using just the websocket session. – VladiC4T Jun 02 '20 at 22:08
  • @VladiC4T Pretty much this. – Oleg V. Volkov Jun 03 '20 at 04:43
2

Here's a slightly alternative view of how these issues could be tackled. I am assuming that authentication and session management is implemented properly.

What if the attacker decides to tamper the "from:id" such that it could send arbitrary messages to anyone from any user?

If you generate a unique (long, random, very difficult to guess, like a session identifier) identifier for each "chat room" at the time of creation and make sure all parties are happy to join that chat room, you could use that instead of user identifiers and control which chat rooms each user could message, to ensure that others cannot send content to someone else's private chats; So your messages from users X and Y would be issued to chat room A and the application would send them across. User Z has not been allowed in so the application refuses to pass the message.

What if the attacker builds a script that spams millions of messages by taking advantage of the "to:id" field?

Make sure that messages cannot be addressed to user identifiers and work towards making user identifiers difficult to guess.

Pedro
  • 3,911
  • 11
  • 25
2

What if the attacker decides to tamper the "from:id" such that it could send arbitrary messages to anyone from any user?

Another option is to give each user a set of public and private keys. These can be used to generate a signature for each message which verifies the contents haven't been tampered with and originated from the specified user.

Let's say user 1 wants to send a message to user 2, a simplified process would be:

  • user 1 is given a public/private key pair. They (or the server) expose their public key to other users.
  • user 1 creates the message content and then generates a signature for it using their own private key (they keep this secret)
  • user 1 sends the message in a packet that looks something like
{ "Signature": "kA7dagf4...", Content: {date: '2020-05-31', time: '14:28:05', text: "Hey!"...
  • user 2 receives the message and then uses user 1's public key to verify the message content matches the signature

The key thing is that the public key can only be used to verify the signature - it's not possible to create a signature without the private key.

Any malicious actor who wants to impersonate user 1 and send a message to user 2 would be unable to, because they won't be able to create a signature that is verified by user 1's public key. So user 2 would see the signature is invalid and be able to reject the message when they receive it.

This is roughly how JSON Web Tokens work - I'd suggest reading up on that for more information - https://jwt.io/introduction/

What if the attacker builds a script that spams millions of messages by taking advantage of the "to:id" field?

As mentioned in previous answers, a combination of rate limiting and making the to:id and from:id fields difficult to guess.

Barker1889
  • 51
  • 3
  • A small issue: where the private key will be stored? If it's stored in the server then it can impersonate users. Even if it's stored in the browser, if you use JavaScript the server can still access it. If the server can be trusted then using public keys seems overkill. – Gustavo Rodrigues Jun 01 '20 at 17:43
  • I may have incorrectly made the assumption that the messaging was peer-to-peer. If the server can be trusted then the above ideas about using a session identifier are the simplest way to solve this. – Barker1889 Jun 04 '20 at 09:44
2

There are numerous security issues in your approach, most already pointed out in other answers.

I want to answer with general principles that will help you find these issues by yourself.

treat every user-supplied data as malicious

everything coming from a client is untrusted. It needs input validation, trimming, escaping, the whole nine yards. In your case, your app probably sends proper JSON, but what happens in your API if someone hand-crafts a JSON and gives you invalid JSON, doesn't terminate the string or mixes SQL injection in there?

never take input on data you already have

as pointed out in other answers, you already know the date/time and the "from" ID, so don't accept them as input. In general, never accept input on something that you can get from a more trustworthy source.

SWIFT approach

go through every element and ask yourself "what could possibly go wrong?". SWIFT (here or several other sources) is a structured way of doing that. Essentially, once you've reduced your input to text and to-ID, think about how someone could abuse those. Could he send wrong data, too little data, too much data? This approach should land you at the threats outlined in other answers, such as enumeration, flooding/spamming, etc.

consider your backend system

finally, know the weaknesses of your backend system. If you have an SQL database behind, think if there are possibilities for SQL injection. Also think about performance and system limits - can a user potentially send so much data that it overwhelms your I/O, your processing or your storage capacity? Can he block the API for other users (what's your parallel processing limits? how many connections can you handle, etc.)

While that's not a full threat modeling approach, I find that it serves 90% as good with a small amount of the full effort.

Tom
  • 10,124
  • 18
  • 51
  • "know the weaknesses of your backend system." I would generally extend that to the whole system. For example, other users could be vulnerable to Cross-Site Scripting (XSS) in the frontend. – Lukas Jun 03 '20 at 21:49
1

An obvious one is that the data is not encrypted. You've already mentioned tampering and often encryption and integrity are addressed at the same time because encryption without integrity still leaves you open to attack

Add a MAC (message authentication code) for the data. Some encryption modes like GCM (Galois/Counter Mode) include one, others are separate so you might use HMAC with something else. Kill 2 birds with one stone as it were, or simply use 2 stones. Will this protect the user from an attacking on your side of the API though? You have to think about what happens if you are compromised too.

You might look at other kinds of API and see how they've mitigated types of attack. For instance, OAuth 2 uses a state parameter and a nonce, for differing reasons. As with @vidarlo's answer, you could use a nonce in combination with the session ID.

ian
  • 1,302
  • 11
  • 21
  • 2
    websockets are trivially encrypted, just like https, and has integrity baked-in. there's also no need for a session because it's not a req/res cycle, it's a constant connection. – dandavis Jun 01 '20 at 04:28
  • 1
    @dandavis I take your point but they're encrypted between the client and the server, not client to end recipient, which is why I added the part about "your side of the API". You're right about the session ID, that would only be needed during the connection set up phase but [websockets are vulnerable to hijack via CORS](https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html), so the part about OAuth's state param is also relevant as is using a session ID. As a user I want to know my message gets to the other end properly, not just the server. – ian Jun 01 '20 at 04:39
  • Client to server encryption is the important part. Client to end-recipient encryption is only important if it actually matters, and depending on the use case end-to-end encryption often is not worth the effort or helpful. – Conor Mancone Jun 01 '20 at 18:05
  • @ConorMancone "Client to end-recipient encryption is only important if it actually matters" - do you work for a 3 letter agency or an advertiser? Otherwise, your statement makes no sense. – ian Jun 01 '20 at 19:45
  • @Iain How about a messaging system inside a CRM? Or a business software suite? Or any number of applications outside of person-to-person communications? There are always more use cases than the one you have in mind, which is why universally recommending just about anything is a bad idea. There are many cases where E2E is not worth the trouble, and more cases where it is contrary to business needs – Conor Mancone Jun 01 '20 at 23:15
  • @ConorMancone From the question: "The App itself is a chat application". E2E is well established as a *reasonable* feature for chat apps now, perhaps even a standard - even Facebook messenger plans to move to it, if the shareholders don't nix it. Prioritising business needs is a good thing and whether this is a high priority or not, or even contrary to business needs is debatable, but this is a security forum. – ian Jun 02 '20 at 01:34
  • There certainly are many cases where E2E encryption is a necessity. I'm not arguing that. Again though, I've built chat applications for business software where you definitely wouldn't want E2E encryption. I can also think of some use-cases in consumer-only systems where E2E encryption isn't what you would want. My point is that, like all things in security, a proper risk analysis and understanding your use case is important here. E2E encryption isn't relevant in all cases, even if it is relevant in many. – Conor Mancone Jun 02 '20 at 09:22
  • -1 because it while a valid concern, the actual question asked was about a malicious **user** (Alice), not a malicious intermediary (Eve). – Tom Jun 02 '20 at 09:34
  • @Tom "the actual question asked was about a malicious user" No it's not, that's an example. "to protect the app from any attacker" is what is stated in the question. You can put that vote back now ;-) – ian Jun 03 '20 at 00:59
  • @Iain I re-read the question and I don't see the OP being worried about MitM or any other **channel** attacks. I still think his concern is with data structures. – Tom Jun 03 '20 at 05:50
  • @Tom How did you not see "to protect the app from ***any*** attacker"? Or *What if the attacker builds a script that spams millions of messages by taking advantage of the "to:id" field?* How is that about *only* the data structure? It's not, rate limiting would be a completely normal mitigation that doesn't affect the caller's data structure. – ian Jun 03 '20 at 06:33
  • 1
    @Iain I still don't believe that MitM is within the scope of the question, but I'll give this the benefit of the doubt. – Tom Jun 03 '20 at 14:50
  • @Tom Very magnanimous of you, and I respect that you hold to your opinion. – ian Jun 05 '20 at 02:07
0

Rule 0: Never trust the client. Validate all inputs from the client side under all circumstances.

In this instance that means checking that the sending user is (a) authenticated as who they claim to be sending the message as, and (b) are authorized to send a given message, based on your criteria. It also means that the "text" field has to be sanitized before being stored or displayed to anyone, and that the timestamp for sending time should be set by the server - as far as your system is concerned a message was only "sent" when the system received it from the sender.

After trimming the parts of the model off that the server can (and should) fill in for the user, really what you have is just the recipient ID, and the message content.

As far as concerns about enumerating the user list by using sequential IDs and/or spamming, there are multiple ways of handling that, such as a "friend request" (by email, phone, username, etc) system that limits users to only being able to send messages to pre-authorized recipients and makes no indication of whether the target of a friend request is an actual user in the system. Additionally you can do traditional rate limiting with something like a leaky bucket, or even build a monitoring system that flags/bans users that are exhibiting flood/spam behaviors.

Garandy
  • 111
  • 1