MarthinL

MarthinL

Nimbleparsec for user input?

Hi folks,

Currently I’m considering passing most if not all my site’s user inputs through a parser specified using NimbleParsec but I’m truly ill-equipped (as per the Dunning-Kruger Effect) to judge whether that’s brilliant or the worst idea ever, or figure out how to make it safer. Having read through most of the threads about NimbleParsec on this forum I must admit most of it went well over my head, so I’m terribly sorry if in my ignorance I am repeating an already answered question but I can really use some help from the obvious experts on the topic right about now.

I currently have the following definitions:

  url = repeat(
          choice([
            ascii_string([?A..?Z, ?a..?z, ?0..?9, ?-, ?., ?_, ?~, ?:, ?/, ??, ?#, ?[, ?], ?@, ?!, ?$, ?&, ?', ?*, ?+, ?,, ?;, ?=], min: 1),
            replace(string("%28"), "("),
            replace(string("%29"), ")"),
            ascii_string([?%], 1) |> ascii_string([?0..?9], min: 1, max: 3),
            replace(string("%%"), "%")
          ]))
        |> reduce({:binary, :list_to_bin, []})
  text = ascii_string([not: ?<, not: ?>, not: ?=, not: ?[, not: ?], not: ?(, not: ?)], min: 1)
  lhook = string("<<")
  equiv = string("==")
  rhook = string(">>")
  delims = choice([lhook, equiv, rhook])

  defcombinator :text,
            empty()
            |> lookahead_not(delims)
            |> concat(text)
            |> unwrap_and_tag(:text)

  defparsec :ribbon,
            ignore(lhook)
            |> tag(repeat(lookahead_not(equiv) |> parsec(:node)), :label)
            |> ignore(equiv)
            |> tag(repeat(lookahead_not(rhook) |> parsec(:node)), :content)
            |> ignore(rhook)
            |> tag(:ribbon)

  defparsec :ribref,
            ignore(lhook)
            |> unwrap_and_tag(integer(min: 1, max: 19), :tid)
            |> ignore(rhook)
            |> unwrap_and_tag(:ribref)

  defparsec :link,
            ignore(string("["))
            |> tag(repeat(lookahead_not(string("](")) |> parsec(:node)), :label)
            |> ignore(string("]("))
            |> unwrap_and_tag(lookahead_not(string(")")) |> concat(url), :url)
            |> ignore(string(")"))
            |> tag(:link)

  defcombinator :node,
            choice([
              parsec(:ribbon),
              parsec(:ribref),
              parsec(:link),
              parsec(:text)
            ])
            |> post_traverse({:node_handler, []})

  defparsec :nlt,
            repeat(parsec(:node))
            |> eos()

The ultimate objective being to parse “NLT” which stands for Non-linear Text - a type of rich/structured text endemic to my system. The idea is to create a somewhat similar experience to Markdown in that users would type regular text but have buttons on the form which would inject the required syntax when they wish to type up some recursive non-linear text to be saved to the database.

I realise the application domain would be unfamiliar to most, so I’ll explain in english what the grammar is supposed to recignise. In essense, a plain old string is the simplest form of non-linear text. Then the string might contain, much like in Markdown, a hyperlink in the format [label](url) which isn’t all that challenging but a good reference point because it means a sequence of nodes, where each node is either just text or a hyperlink is also valid non-linear text. Then we introduce two more options. The first being <<label==content>> which is called a ribbon. Ribbons are (at least two) separate entities and both the label and content portions are recursively sequences of nodes. The user can create these ribbon on the fly by using the syntax in the text they input. As we process it, we’d create the required ribbon component entities and replace the original ribbon definition with an abbreviated ribbon definition in the (fourth and final) form <<id>> where the id is simply a bigint database id of the leading component. The <<id>> format may refer to ribbon data that was created by the current form or to pre-existing ribbon data. And that is it. I don’t intend to allow any user text to be “executed” or sent directly to database and have the database infrastructure to validate that the user is allowed access to referenced links and ribbons, but this is very much parsing user text so it would no longer be true to say that what the user inputs isn’t parsed and that’s where I perceive the danger I cannot quite wrap my head around.

As an asside, I first tried using the Md library because what I’m trying to achieve felt so much like Markdown that I thought it would be a good place to start. But I never really felt I was able to control the grammar well enough to have any confidence that it wasn’t leaving gaping holes for all sorts of delierate or accidental abusers to exploit. It seemed there were Markdown syntax that was being recognised even by a supposedly empty spec, so I pivoted to NimbleParsec.

The plan is to incorporate the parsing into the Changeset validations and build the required fully recursive changeset to allow persisting the whole lot to database in a single transaction if it’s valid and have useful visual feedback as to where in the input the syntax stops being valid. That part I’m nowhere near done with so I can’t share that here for a more complete picture.

At this stage I was really just hoping for someone smarter than myself to take a look at the parsers and combinators I’ve defined and point out ways to guard against it become an attack vector to cripple my site. Something like if someone types in just a massive list of < characters it may overrun the stack and crash the system. Or point out why no such parser can ever be hardened enough to get exposed directly to user inputs. I can still back out of this approach if I have to even though it really seems elegant and effective from my current (poorly-informed) perspective.

Thank you kindly, in advance.

First Post!

Eiji

Eiji

As long as you do not write SQL expressions by hand or allow user to write it (in short nothing related to nimble_parsec) as long I would not worry about security problems.

While there is no rule to cover all edge cases, every time you think about manipulating large structures in Elixir (which does not support mutable variables) you should consider simplifying especially having in mind Elixir is best for working with a huge number of small messages (in opposite to small number of huge messages). If you are in edge case and believe this is best for you then all good.

The only place where things could go really wrong is repeat, but not because it’s bad. Think that user input is not normal, but have few, dozens, hundreds or even thousands of MB. The nimble_parsec would not solve for you every possible problem. If you have problem in other part of app (SQL injection, missing input validation before sending to server like limit number of characters) then no parser could help you no mater how well written it is or what library it use.

For example when you would inject a parsed value as table name then you could have a SQL injection. Think that parser is supposed to parse <table-name> by taking the table name between less than (<) and greater than (>) characters ignoring hyphen (-) character. In normal case everything works as expected, but one time you could receive partial SQL which closes table identifier and ends with something like delete * from users; - that would happen no matter how well you write parser. However if you pass insecure variable to ecto’s query API then the adapter should escape input correctly, so you don’t have to worry about SQL injections.

As you can see unless your parser would be huge and catches all possible edge-cases (like SQL injections) no matter if the input is correctly parsed or not the other part of app could have a problem. In general you should double-check if user input is properly secured for example by setting maxlength to input element. Best if the validation would happen on client side, so the users would not lose their time re-entering all form fields. parser (this or other) is always well seen. nimble_parsec should generate one of the fastest if not fastest parsers, but as said it would not cover literally everything which is really fine.

Most Liked

MarthinL

MarthinL

I’ve taken that to mean that the server-side code would perform sanity checks of its own before throwing text at a parser. Good luck overriding that by editing the HTML.

Stefano1990

Stefano1990

I meant it more like for completeness sake, so if a beginner reads this thread they don’t think that simply setting the max length on the input is some kind of validation.

MarthinL

MarthinL

You’d have noticed me saying I’ve long wanted to write that, not that I have. I’m rather well aware that feedback is an enabler, most for good people but also for those with bad intentions. Detecting, with some degree of certainty, attempts to disrupt service, but either withholding feedback or deliberately giving misleading feedback is a powerful approach. In the context of my dream error message, that means that the error would not show on the attacker’s screeen while they’re messing about. It would appear as an email well after the fact. As for the immediate response to an eror like that, it’s probably best to let the attacker believe they’ve caused the chaos they intended even if it didn’t, like just never sending a response and forcing a restart of the session some minutes later.

Where Next?

Popular in Questions Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54996 245
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement