Invoke `handle_event` in a test as if I'm a malicious websocket hacker?

Hi all,

I have a Live page that shows a form if the user has elevated permission, and that form can be submitted and thus handled by the Live page’s handle_event. So far so good!

But does that automatically mean the handle_event function is secure against a non-elevated user directly manipulating the websocket to issue commands? Could a malicious user replicate the traffic an elevated user sends into the websocket, and thus pretend to submit the data?

When I look at my code I want to add a permission check in the handle_event function, but I don’t know how to write a test that directly sends such an arbitrary event.

Would someone either suggest a way I could test this scenario, or explain how I’m fundamentally asking an invalid question out of ignorance of how the websocket is already secure? :pray:

In most cases you do the authorization process when initializing Live pages. In that cases you simply assign some data to WebSocket like current_user and in case of all events you simply let if fail I guess. If there is no current_user and your code expects it then you would have an errors about nil values or assigns not available in socket when a template is rendered. I’m not sure what are edge-cases that requires any additions steps at the part of handling events.

2 Likes

There is some discussion around this here.

You’re generally safe but even still it’s best to do these checks in the business domain (“context”) layer. There are libraries that facilitate this. My favourite is LetMe.

1 Like

The client cannot by any means directly manipulate assigns, but they can certainly send hand crafted events, absolutely. Data that comes in from handle_event should not be trusted.

That said from a permissioning standpoint, you’d be checking that the current_user has the permission, and that value is from your assigns not from the handle_event params. Obviously you need to make sure on mount to use a reliable authentication method to make sure you set the current_user assign safely.

3 Likes

Thanks Ben that’s quite clear, and matches my understanding. I appreciate your “never trust the client” attitude :slight_smile:

Could you hazard a guess for how we might unit test the client sending a handcrafted event? Even a native Elixir message sent directly into the live view process could work to simulate this I think, but I couldn’t find any way to invoke handle_event.

Thanks for the link, I can’t quite make sense of any conclusions but it’s an interesting discussion!

Your point of moving it into the context is insightful, I’m usually hesitant on such because I think of authorisation as belonging in web-land but… the more I think about it the more your angle makes sense: It’s the only way to bake these deeply business critical constraints into the core product itself.

I’ll think more on it, but thanks for the input!

@gaggle I think using Phoenix.LiveViewTest — Phoenix LiveView v0.20.14 should be sufficient. At the end of the day even though the clients can hand craft events, those events still have to match the valid list of event types that the phoenix socket code expects, or the phoenix socket code will reject the event or just crash.

So to get to your live view at all it has to be a mostly valid event. The suspect part is of course the parameter values / shape. That specific function simulates JS code sending an arbitrary event to your live view from a hook, which gives you that maximum freedom to craft payloads, within the bounds of payloads which will work with the actual socket layer.

You can use render_click|change|submit|hook with your arbitrary inaccessible-from-ui event name:

{:ok, lv, html} = live(conn, ~p"/myroute")

render_click("something_i_cant_send_via_the_markup_you_gave", %{one: 1})
{~p"/home",%{"error" => "You can't access that page"}} = assert_redirect(lv)
5 Likes

I like it in the context since I like to think of the web as “just another client.” Ideally I can treat iex as another client and use my app with ease simply by calling context functions. This also makes testing a little saner. Authorization often involves scoping too, so I find it much more ergonomic to simply pass user, params, and a possible entity to act on to one context function rather than having to additionally call an authorizer. Some people would disagree, though! There are trade-offs, like you always have a user ready in iex.

I do always like recommending LetMe since it lists the alternatives that take slightly different approaches. One it doesn’t mention that I really like is Janus. I ultimately landed on LetMe as my goto since it has a similar flavour to and plays well with Flop (which stands to reason as they are both by (at)woylie).

1 Like

Wow, thanks!, I hadn’t gotten that to work when I played around. It works a treat, test automation now proves our handler is secure.