23

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?

12
  • 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
    Commented Jun 1, 2020 at 6:22
  • 15
    @stefan this is only relevant if user enumeration is a threat. In a lot of scenarios it's not.
    – vidarlo
    Commented Jun 1, 2020 at 9: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
    Commented Jun 1, 2020 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
    Commented Jun 1, 2020 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. Commented Jun 2, 2020 at 7:00

8 Answers 8

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.

6
  • 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
    Commented May 31, 2020 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
    Commented May 31, 2020 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
    Commented Jun 1, 2020 at 4:26
  • 2
    @dandavis that's true, but with mobile browsers it seems that there might be some problems with that approach. Thus, some form of session may not be a bad idea.
    – vidarlo
    Commented Jun 1, 2020 at 9:46
  • 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. Commented Jun 1, 2020 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.

4
  • 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
    Commented May 31, 2020 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
    Commented May 31, 2020 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
    Commented Jun 1, 2020 at 4: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. Commented Jun 1, 2020 at 13:57
11

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.

6
  • 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
    Commented Jun 1, 2020 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
    Commented Jun 1, 2020 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. Commented Jun 2, 2020 at 0: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
    Commented Jun 2, 2020 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
    Commented Jun 2, 2020 at 22:08
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.

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.

2
  • 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. Commented Jun 1, 2020 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
    Commented Jun 4, 2020 at 9: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.

1
  • "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
    Commented Jun 3, 2020 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.

13
  • 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
    Commented Jun 1, 2020 at 4: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, 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
    Commented Jun 1, 2020 at 4: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. Commented Jun 1, 2020 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
    Commented Jun 1, 2020 at 19:45
  • 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
    Commented Jun 3, 2020 at 14:50
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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .