Template Engine Fingerprinting

Posted June 17, 2022 by Charles Bevan ‐ 4 min read

When testing web applications you may come across pages that allow you to enter a template and have it rendered to a user. Without access to the code it may be difficult to determine what the underlying template engine is. Here we will compare common JavaScript template engines to determine techniques to tell them apart without access to the underlying rendering code.

This blog entry will cover the following template rendering engines:

An accompanying test project can be found at https://github.com/cfbevan/template-fingerprinting

NOTE: For the following examples the data passed in to the rendering engine is

{
  "data": {
    "a": "a",
    "b": 1,
    "c": [1, 2],
    "d": true,
    "e": false
  }
}

The Situation

Imagine that you are in the middle of testing a website and you come across a page that allows you to create an email to send to users.

Email input

There are clues on the page that tell you that you are using a templating engine, now it is up to you to find out which one is being used.

Test Rendering Variables

EJS and Pug are unique amongst the others in how they render variables. Mustache, Liquid, and Handlebars all use the same {{ variable }} syntax when rendering.

Pug uses #{ variable } syntax when rendering data. Example p #{data.a} test renders to <p>a test</p>. Passing {{data.a}} to Pug will crash the renderer with an error.

EJS uses <%= variable %> syntax when rendering data. Example <%= data.a %> test renders to a test. Passing {{data.a}} to EJS will output {{data.a}} without rendering any variables.

We start to see some test emerging.

flowchart TD start --> variable("Test '{{variable}}'") variable -->|Crashes| Pug variable -->|Renders original input| EJS variable -->|Renders data| other("Liquid, Mustache, Handlebars")

Test Control Functions

At this point we need to start testing conditional or control flow functions in our rendering. I find that testing the conditionals is easier because it does not require you to know anything about the data. Attempting to loop over non array data has a higher chance of crashing than simply testing for non existent data.

For this test, Liquid is the odd one out. It uses syntax similar to EJS for testing conditionals; {% if variable %}.

Example 1

{% if data.d %}
  in liquid
{% endif %}

Renders as the following in each engine.

Liquid

  in liquid

Mustache

{% if data.d %}
  in liquid
{% endif %}

Handlebars

{% if data.d %}
  in liquid
{% endif %}

Example 2

{% if data.variable_that_does_not_exist %}
  in liquid
{% endif %}

Renders as the following in each engine.

Liquid


Mustache

{% if data.variable_that_does_not_exist %}
  in liquid
{% endif %}

Handlebars

{% if data.variable_that_does_not_exist %}
  in liquid
{% endif %}

Here you can see that Handlebars and Mustache do not like the {% if } syntax. Note that in example 2 we still get information even if we are testing for a variable that does not exist. You do not need to know the data passed to the rendering engine in order to use these techniques. Running a conditional with missing variables will simply treat the if as false and render empty lines. If you get back what you put in you can exclude Liquid.

flowchart TD start --> variable("Test '{{variable}}'") variable -->|Crashes| Pug variable -->|Renders original input| EJS variable -->|Renders data| if1("Test '{%if variable %}in liquid{% endif%}'") if1 -->|Renders 'in liquid' or ''| LiquidJS if1 -->|Renders original input| other("Mustache or Handlebars")

Now we can use the same technique to determine between Mustache and Handlebars.

Mustache uses {{#variable}} for conditions. Example {{#data.d}}mustache{{/data.d}} renders as mustache in Mustache.

Handlebars uses {{#if variable}} for conditions. Example {{#if data.d}}handlebars{{/if}} renders as handlebars in Handlebars. Be careful here. Since Handlebars is so closely related to Mustache the example {{#data.d}}mustache{{/data.d}} still renders as mustache in handlebars as well. However, {{#if data.d}}handlebars{{/if}} crashes if sent to Mustache. Since you can tell the difference between outputs with the Handlebar example, I suggest you use it when testing.

Roundup

With three simple queries we were able to determine which template engine is being used on a website where we don’t have access to the underlying code. The following is a quick flowchart you can follow during your own testing.

flowchart TD start --> variable("Test '{{variable}}'") variable -->|Crashes| Pug variable -->|Renders original input| EJS variable -->|Renders data| if1("Test '{%if variable %}in liquid{% endif%}'") if1 -->|Renders 'in liquid' or ''| LiquidJS if1 -->|Renders original input| if2("Test '{{#if variable}}handlebars{{/if}}'") if2 -->|Crashes| Mustache if2 -->|Renders 'handlebars' or ''| Handlebars

Armed with this information you are now prepared to look for specific vulnerabilities and exploits. You can access the accompanying project https://github.com/cfbevan/template-fingerprinting to test your new found knowledge in a local test environment.