You've reached the third and final entry of the Instantiating arbitrary Qt Quick components with JSON series. Part 1 and Part 2 can be found at their respective links.
The first part focuses on the software design pattern used to dynamically instantiate components. The second one shows how to layout these dynamic components by incorporating QML' s positioning and layout APIs.
Part 3: Anchors and the Dangers of Remote Code Execution
On the last entry I wrote about:
- Coordinate positioning
- Item positioners
- Qt Quick Layouts
Now there's:
Unfortunately for us, this is were the QML Engine becomes a limiting factor. Items in QML, instantiate QObjects. Every QObject in Qt contains an ID. We can set that ID from QML the moment we instantiate our component, like so:
Nevertheless, id is not a normal QML property. In fact it is no property at all. As a result, we're unable to set an id after instantiation. Since neither Repeater or Instantiator allows us to set individual IDs during object creation, we have no means for naming our instantiated components, and therefore we cannot refer to them from other components. This limits our ability to anchor components to siblings that come from the repeater. We can only anchor to the parent and to siblings defined traditionally from the QML document.
With this in mind, let's see how far can we take the use of anchors in our components factory.
But first, a quick reminder... Qt provides us with two syntaxes for using anchors, an object syntax and a shorthand for its individual properties:
To keep things simple, I make use of the longer syntax in our model:
Let's start by implementing fill: parent. If we were to assign the string from our model, which says "parent", to our instantiator, the assignment would fail because we cannot assign a string where an object id is expected.
Instead, we must parse the value from our model into valid JavaScript, and for that we use Javascript's eval function. Calling eval will return whichever value the JS expression inside the string evaluates to. Here "parent" will point to the parent property relative to the context where eval is run. For the purposes of anchoring, this is fine.
Our code now looks like:
There's a couple of issues:
- Qt Creator's annotator suggests not to use eval. You may ask: Why is this? Because eval treats the string from our model as a JavaScript expression. Nothing prevents a malicious actor from injecting JS code into that string that could attach a payload to our app to our app to later perform a remote code execution attack. A malicious actor might write something like:
This takes care of the parent assignment, then overrides the contents of onPressed with a function that runs arbitrary code for the user to unsuspectingly execute. This is why every respectable linter and static analyzer for JavaScript will warn you not to use eval or any of its equivalent methods; in order to prevent remote code execution.
- If security is not a big enough concern for you, eval cannot be parsed by the new JavaScript compilers introduced with Qt 6. The compiler will error out saying:
Part 4: Defeat Remote Code Execution by Sanitizing Inputs
My preferred way to sanitize inputs is using Regular Expressions. We want to guarantee the strings from our models evaluate to a valid anchor name.
Valid anchor names include:
- parent
- idName.top
- idName.right
- idName.bottom
- idName.left
- idName.baseline
- idName.verticalCenter
- idName.horizontalCenter
Here's a RegEx I wrote that covers all anchors:
We can use it to sanitize anchor inputs from the model, like so:
The key is to remove all characters that could be used to execute malicious content. If one of such characters must be present, then we make sure its uses are restricted, as we've seen here.
Here's what my final code for attaching anchors looks like:
Like with Layouts, we attach our anchors to the instantiator, and not just the item. The item must attach itself to the instantiator where applicable.
Then take care of the rest of the anchor's properties like so:
As you can see, we nullify the dangers of eval by sanitizing our inputs properly. This doesn't fix our inability to anchor to sibling components or our inability to pre-compile the QML, but we can use this tool to refer to other items in the QML tree. The tree itself may come from a document loaded from a remote location, using a Loader.
Running any remote document in our QML code would also open the doors to arbitrary code execution attacks. We may not be able to sanitize an entire QML document like we can sanitize for individual properties, therefore you should only allow QML and JS file downloads from a trusted source, and always use an encrypted connection to prevent a targeted attack. Self-signed certificates are better than no certificate. Without encryption, a malicious actor could intercept the traffic and alter our code while it's on its way.
This has been the last entry in this series. Thanks to Jan Marker for his time reviewing my code. I hope you've learned something from it. I personally enjoyed looking back at Part I the most, because the technique shown there has more real world applications.
Reference
- Learn how to write Regular Expressions by playing with the editor and cheat sheet at https://regexr.com/
The more precise you learn to make your definitions, while attempting to keep them small, the better you'll get at writing them.
Trusted software excellence across embedded and desktop platforms
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
4 Comments
20 - Jun - 2024
Grecko
Hello,
Interesting project, is the final code available somewhere? I'd like to play with it.
You wrote that valid anchor names includes idName. but also mention that "we cannot refer to [our instantiated components] from other components". Does your eval logic works with anything other than parent as the id then? I feel like this could be done without eval and adding for loops would simplify the code.
27 - Jun - 2024
Javier Cordero
Hi Grecko,
I would rather not host the full code because I worry that would legitimize it. The techniques I focused on during the conclusion of each blog post are good things on their own, but people should steer away from using tree models to instantiate arbitrary Components from QML, as that would produce code that's difficult to maintain and develop on top of. It also goes in a direction contrary to The Qt Company's efforts in making QML compilers.
Having said that, I made sure one could re-create a working project only by copy-pasting code from each of the blog entries. So, you should be able to get a working project by reading carefully and copy-pasting the right snippets.
The eval logic will work with any id that can be accessed from directly inside of the instantiator component, because "instantiator." is a prefix in all of the calls to eval. The prefix is only there because anchors in QML have a limitation: they will only work with components that are adjacent to them or with the parent, but we cannot anchor to the parent's parent, the root element, or to the child of a sibling. Meaning you could forgo the prefix in other uses.
Now, the reason I said “we cannot refer to [our instantiated components] from other components” is unrelated to that. The components from the model are being instantiated via an instantiator. Both instantiators and Repeaters will not allow us to set ids to their components. Defining a property and calling it id won't do because ids are not properties. Since we can't and therefore don't give the components an id, "we cannot refer to [our instantiated components] from other components".
You're absolutely right about using for loops to simplify or shorten the code. Nevertheless, eval would still be needed for the anchors because ids are different from properties.
1 - Jul - 2024
Grecko
So here we can only anchors to the parent and not the siblings since we can't set an id to the siblings. In pseudo JS it could be done like this:
No need for an eval here because we always anchors to parent. In your code you do :
instantiator.anchors.centerIn = eval("instantiator." + modelData.anchors.centerIn)
butmodelData.anchors.centerIn
would always be "parent" unless I'm mistaken.1 - Jul - 2024
Javier Cordero
Nice strategy. Given the value would always be parent, we could get rid of eval that way. I stand corrected.