wktdev

wktdev

I have a collection named Testbeds and various keys on this collection. Some of these are named hardware,name,status,database etc.

In a Live View , in the DOM I iterate through Testbeds and render content assigned to the various collections.

 def render(assigns) do
<div>

<%= for testbed <- @testbeds do %>
  # Each test bed is displayed with the different collection values

<% end %>
</div>

My problem is , I wrote a function to sort the testbeds based on a given collection but when I try to abstract it so I can use it with the DOM I get unpredictable sorting behavior,

The function as-is works but the collection properties are hard coded.

Example (this works)

  def handle_event("sort_by_string", %{"field" => field}, socket) do

    if socket.assigns[:sort_by_name_ascending] == false do
      sorted_testbeds = Enum.sort_by(socket.assigns.testbeds, fn item -> Map.get(item, :hardware) end)   #hardware is hard coded as an atom
      {:noreply, assign(socket, testbeds: sorted_testbeds, sort_by_name_ascending: true)}
    else
      sorted_testbeds = Enum.sort_by(socket.assigns.testbeds, fn item -> Map.get(item, :hardware) end) |> Enum.reverse()  #hardware is hard coded as an atom
        dbg(sorted_testbeds)
      {:noreply, assign(socket, testbeds: sorted_testbeds, sort_by_name_ascending: false)}
    end
  end

When I replace :hardware with a string it doesn’t “work”. I expect it to sort alphabetically and when :hardware is set as an atom it works as expected. When replaced as a string I get something else.

My goal is to be able to abstract the function so I can attached it to an element and sort by any collection.

<th scope="col" class="px-6 rounded-l-full " phx-click="sort_by_string" phx-value-field="hardware" >Hardware</th>

Showing Posts 1 to 9

garrison

garrison

Are your testbed items maps with atom keys? e.g.

%{
  hardware: "foo",
  something: "bar",
}

Maps with atom keys and string keys are not equivalent. Map keys do not have to be atoms (they can be any term). See the docs:

You can either convert the maps to use string keys or convert your field string into an atom. You may be tempted to use String.to_atom/1 for this, but you shouldn’t because a malicious user could use up all of your atoms (see the docs).

A simple alternative is to do this:

field = case field do
  "hardware" -> :hardware
end

Or you can use String.to_existing_atom/1, but then conventional wisdom is that you have to be careful to define the atom previously in the module (again, see the docs).

garrison

garrison

By the way, the reason your code doesn’t blow up is that Map.get/3 returns nil by default if the key doesn’t exist. So you are sorting all of your items by the same value, meaning the returned order is the underlying order of the input.

If you had used Map.fetch!/2 instead, you would have received an error. In this particular case that would be better practice.

garrison

garrison

Oh, and also: Enum.sort_by/3 accepts :asc or :desc as a third argument, which would be much more efficient than sorting and then reversing the list :slight_smile:

wktdev

wktdev OP

So I need to either go through the entire collection and convert all the keys to strings or alternatively risk blowing up the app because their is a limited number of available atoms?
I appreciate the reply but something about this doesn’t sound right.

I didn’t explicitly mention that the collection was from Ecto.
It seems like Phoenix would anticipate users matching param strings to keys in Ecto.

garrison

garrison

No, definitely not! If you re-read my post you will see I provided you with two safe options to convert a string to an atom. I was just trying to warn you about the unsafe option (String.to_atom/1) because I assumed from the title that you were new and might not be aware!

Just to be clear, there is a limited number of unique atoms. Since your LiveView events process arbitrary user data, a malicious actor could spam your event handler with random strings and fill up your atom table. That is why you can’t use String.to_atom/1 directly.

As long as you first validate that field is one of a limited number of values, it would be fine. A simple case statement is usually what I use, and so it’s what I suggested first.

If I had known the maps were Ecto structs I would not have bothered suggesting converting the keys to strings :slight_smile: Obviously converting field to an atom is the better approach here.

Ecto is designed to convert arbitrary data (string keys) to structured data (structs with atom keys, which are compile-time validated). They don’t support access by string key, but they do support parsing and validating string-keyed data (via changesets) into structured data.

wktdev

wktdev OP

Thanks for this.

I assume the code below is generally protected from an “atom spam” attack , correct?

  def handle_event("sort_by_string", %{"field" => field}, socket) do

     atomParam = String.to_existing_atom(field)  

    if socket.assigns[:sort_by_name_ascending] == false do
      sorted_testbeds = Enum.sort_by(socket.assigns.testbeds, fn item -> Map.get(item, atomParam) end)   #hardware is hard coded as an atom
      {:noreply, assign(socket, testbeds: sorted_testbeds, sort_by_name_ascending: true)}
    else
      sorted_testbeds = Enum.sort_by(socket.assigns.testbeds, fn item -> Map.get(item, atomParam) end) |> Enum.reverse()  #hardware is hard coded as an atom
        dbg(sorted_testbeds)
      {:noreply, assign(socket, testbeds: sorted_testbeds, sort_by_name_ascending: false)}
    end
  end

I am a bit confused how I would use that case statement but assuming String.to_existing_atom(field) works just as well, I prefer it as it’s more explicit.

I did some quick searching and the googles returned this blurb:

In Erlang and Elixir, you should avoid using atoms when: you are dynamically generating a large number of unique atoms based on user input or external data, as this can potentially lead to “atom exhaustion” where the system runs out of memory due to the limited number of possible atoms that can exist within a single Erlang VM instance; in such scenarios, consider using strings and converting them to atoms only when necessary using String.to_existing_atom/1 to ensure only pre-defined atoms are used.

garrison

garrison

Yes, as I mentioned before, String.to_existing_atom/1 is safe. The function does come with a caveat, though: because there is no way to guarantee the order in which modules are loaded, there are situations where, unless you have previously declared the atom somewhere in that module, the function may fail. See the docs.

I don’t think this will apply to your situation, though, as by the time this function is called the atom will almost certainly exist. Maybe you could get a weird case where the testbed list is empty and no module referencing the atom has been loaded yet, at least in development, but I’m not entirely sure.

However, I still recommend the case approach instead. Your code allows a malicious user to effectively pass in any atom. It is much safer to validate the possibilities up-front. For example, imagine, in the future, that you added a field to your schema which you did not want users to be able to sort by. Well, a malicious user could pass any atom into your function, including that particular field.

This might not sound so bad in this particular case, but it’s not hard to imagine similar code accidentally, say, leaking the password field from a User struct. Best to avoid this class of issues entirely and validate your inputs early.

def handle_event("sort_by_string", %{"field" => field}, socket) do
  field = case field do
    "hardware" -> :hardware
    "foo" -> :foo
    # ... etc
  end
  # ... the rest of your code
end

This is not the only way to validate your inputs. For example you could, equivalently, do this:

def handle_event("sort_by_string", %{"field" => field}, socket)
    when field in ["hardware", "foo"] do
  field = String.to_existing_atom(field)
  # ... the rest of your code
end
wktdev

wktdev OP

Thanks again!

I feel like Phoenix should have a “safe mode” like JavaScript. This feels like too much to keep track of.

garrison

garrison

JavaScript’s “strict” exists to remedy a number of language footguns which exist because JS as a language was cobbled together in like a week and is, in general, pretty busted. Even in strict mode JS has far, far more footgun-type issues than Elixir. For example: 0 is falsey, 64 bit integers don’t exist (ORMs have to use strings, it’s ridiculous), the == vs === situation, etc.

Elixir code is actually very safe because as Elixir developers we often write our code with a lot of explicit validation. For example, the case statement I showed you will raise an error if an untrusted value is passed in - same for the guard version I provided. Most other programming languages are not so explicit about such things (especially JavaScript).

The need to validate your inputs, though, has nothing to do with Elixir or Phoenix. If you were writing your application in JavaScript you would still have to validate what the client sends you - and it’s the same for any framework! Learning to write code in this way helps prevent security issues. It’s a defense-in-depth strategy.

If you had ignored my advice above and used your code, you probably would have been fine. But because this is a forum where people come to learn, it’s important that we provide advice which follows best practices. Sometimes I’m wrong too and someone else corrects me :slight_smile: But if that happens, it’s important that I go back and fix my post, otherwise someone could pop in from a search engine, learn the wrong thing, and close their tab without ever finding out!

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
widianto
I think I’ve found a small improvement I could contribute to &lt;%= web_namespace %&gt;.CoreComponents (installer/templates/phx_web/compo...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
aseigo
ICal is a library for interacting with iCalendar data. It parses iCalendars into typed Elixir structs via ICal.from_ics, and can prepare ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews