Skip to content

Anchoring Qt Quick Components Instantiated with JSON Factory Design Techniques - Parts 3 & 4

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:

  • Anchors

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:

Item {
    id: myID
}

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:

anchors.left: parent.right
anchors: {
    left: parent.right
}

To keep things simple, I make use of the longer syntax in our model:

// This model lays out buttons vertically
property var factoryModel: [
    {
        "component": "RowLayout"
        "anchors": {
            "fill": "parent"
        }
        "children": [
            {
               "component": "Button"
               "text": "Button 1"
            },
            {
               "component": "Button"
               "text": "Button 2"
            }
        ]
    }
]

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.

// This would fail...
instantiator.anchors.fill = modelData.anchors.fill;

// because it evaluates to this:
instantiator.anchors.fill = "parent";

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:

if (typeof(modelData.anchors.fill) === "string")
    instantiator.anchors.fill = eval("instantiator."
                                     + modelData.anchors.fill);

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:
"parent;this.onPressed=function(){/*arbitrary_code_goes_here*/};"  // Note that I don't know if this example really works

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:

error: The use of eval() or the use of the arguments object in signal handlers is not supported when compiling qml files ahead of time. That is because it’s ambiguous if any signal parameter is called “arguments”. Similarly the string passed to eval might use “arguments”. Unfortunately we cannot distinguish between it being a parameter or the JavaScript arguments object at this point.

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:

const anchorsRegex =
/[a-z_][a-zA-Z_0-9]*(\.(top|right|bottom|left|baseline|(vertical|horizontal)Center))?/;

We can use it to sanitize anchor inputs from the model, like so:

anchorsRegex.test(modelData.anchors.fill))

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:

Component {
  id: loaderComp
  Loader {
    id: instantiator
    // ...
    onItemChanged: {
  // Anchor properties
  if (typeof(modelData.anchors) === "object") {
    // Regex for validating id and id.validAnchorline
    const anchorsRegex = /[a-z_][a-zA-Z_0-9]*(\.(top|right|bottom|left|baseline|(vertical|horizontal)Center))?/;
    // Before attempting to assign,
    // check that properties are present in object
    // and that model attributes aren't undefined.
    if ((typeof(modelData.anchors.fill) === "string")
      && anchorsRegex.test(modelData.anchors.fill)) {
      instantiator.anchors.fill = eval("instantiator."
                                         + modelData.anchors.fill);
    }
    else if ((typeof(modelData.anchors.centerIn) === "string")
      && anchorsRegex.test(modelData.anchors.centerIn)) {
      instantiator.anchors.centerIn = eval("instantiator."
                                         + modelData.anchors.centerIn);
    }
    else {
      if ((typeof(modelData.anchors.left) === "string")
        && anchorsRegex.test(modelData.anchors.left)) {
        instantiator.anchors.left = eval("instantiator."
                                         + modelData.anchors.left);
        item.anchors.left = item.parent.anchors.left;
      }
      if ((typeof(modelData.anchors.right) === "string")
        && anchorsRegex.test(modelData.anchors.right)) {
        instantiator.anchors.right = eval("instantiator."
                                         + modelData.anchors.right);
        item.anchors.right = item.parent.anchors.right;
      }
      if ((typeof(modelData.anchors.baseline) === "string")
        && anchorsRegex.test(modelData.anchors.baseline)) {
        instantiator.anchors.baseline = eval("instantiator."
                                         + modelData.anchors.baseline);
        item.anchors.top = item.parent.anchors.top;
      }
      else {
        if ((typeof(modelData.anchors.top) === "string")
          && anchorsRegex.test(modelData.anchors.top)) {
          instantiator.anchors.top = eval("instantiator."
                                         + modelData.anchors.top);
          item.anchors.top = item.parent.anchors.top;
        }
        if ((typeof(modelData.anchors.bottom) === "string")
          && anchorsRegex.test(modelData.anchors.bottom)) {
          instantiator.anchors.bottom = eval("instantiator."
                                         + modelData.anchors.bottom);
          item.anchors.bottom = item.parent.anchors.bottom;
        }
      }
    }
    // ...

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:

        // ...
        // Anchor number properties
if (typeof(modelData.anchors.margins) !== "undefined")
  instantiator.anchors.margins = Number(modelData.anchors.margins);
if (typeof(modelData.anchors.leftMargin) !== "undefined")
  instantiator.anchors.leftMargin = Number(modelData.anchors.leftMargin);
if (typeof(modelData.anchors.rightMargin) !== "undefined")
  instantiator.anchors.rightMargin = Number(modelData.anchors.rightMargin);
if (typeof(modelData.anchors.topMargin) !== "undefined")
  instantiator.anchors.topMargin = Number(modelData.anchors.topMargin);
if (typeof(modelData.anchors.bottomMargin) !== "undefined")
  instantiator.anchors.bottomMargin = Number(modelData.anchors.bottomMargin);
if (typeof(modelData.anchors.horizontalCenterOffset) !== "undefined")
  instantiator.anchors.horizontalCenterOffset = Number(modelData.anchors.horizontalCenterOffset);
if (typeof(modelData.anchors.verticalCenterOffset) !== "undefined")
  instantiator.anchors.verticalCenterOffset = Number(modelData.anchors.verticalCenterOffset);
if (typeof(modelData.anchors.baselineOffset) !== "undefined")
  instantiator.anchors.baselineOffset = Number(modelData.anchors.baselineOffset);
      }
    }
  }
}

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.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

Categories: KDAB Blogs / KDAB on Qt / QML / Qt

Tags:

4 thoughts on “Anchoring Qt Quick Components Instantiated with JSON”

  1. 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.

    1. Hi Grecko,

      > Interesting project, is the final code available somewhere? I’d like to play with it.

      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.

      > 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?

      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”.

      > I feel like this could be done without eval and adding for loops would simplify the code.

      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. > 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.

        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:

        for (let [key, value] in modelData.anchors) {
        if (key in [“fill”, “centerIn”] && value === “parent”)
        instantiator[key] = instantiator.parent;
        const anchorLines = [“left”, “right”, “top”, “bottom”, “baseline” ];
        if (key in anchorLines) {
        let [item, line, excess] = value.split(‘.’);
        if (item === “parent” && line in anchorLines && excess === undefined)
        instantiator[key] = instantiator.parent[line];
        }
        if (key in [“margins”, “leftMargin”, …] && typeof value === “Number”)
        instantiator[key] = value;
        }

        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)` but `modelData.anchors.centerIn` would always be “parent” unless I’m mistaken.

        1. Nice strategy. Given the value would always be parent, we could get rid of eval that way. I stand corrected.

Leave a Reply

Your email address will not be published. Required fields are marked *