Tools: Day 5 — Intentionally Building and Breaking an SSTI Vulnerability (Flask + Jinja2)

Tools: Day 5 — Intentionally Building and Breaking an SSTI Vulnerability (Flask + Jinja2)

Source: Dev.to

What is SSTI? ## Goal of the Lab ## Part 1 — The Secure Version ## app_secure.py ## templates/message.html ## Why This Version is Secure ## Test Payload ## Part 2 — Intentionally Introducing the Vulnerability ## app_vulnerable.py ## What Changed? ## Verification ## Proof of Server Access ## Understanding the Vulnerability Chain ## Why the Bug Exists ## Key Lessons Learned ## 1. Framework Defaults Matter ## 2. Never Build Templates from User Input ## 3. SSTI is More Than a Web Bug ## 4. Security is About Context As part of my ongoing rebuild of fundamentals, Day 5 moved away from solving CTF machines and into something far more educational: I created my own vulnerable machine. For the past few days I had been identifying bugs in other people’s systems. Today I wanted to understand something deeper: Not just how to exploit vulnerabilities but how developers accidentally create them. The focus was Server-Side Template Injection (SSTI) using Flask and Jinja2. Instead of immediately making a vulnerable application, I deliberately built two versions: This made the lesson much clearer than reading theory. I wasn’t memorizing payloads anymore. I was watching security break in real time. Normally, a web server renders HTML templates and inserts user data into them. The server replaces {{ username }} with text. But if user input becomes part of the template itself, the server stops rendering a page and starts executing instructions. That is Server-Side Template Injection. In Jinja2, expressions inside {{ }} are evaluated as Python-accessible objects. So if an attacker controls what appears inside those braces, they may reach the Python runtime. I created a minimal “blog message page”: The server displays it back. Simple enough to understand… but powerful enough to demonstrate a full compromise chain. This small design let me practice: I first built the correct implementation. render_template() loads a static template file and passes variables separately. Jinja2 treats name and msg as data. It also auto-escapes HTML. Jinja refused to interpret the payload because the template structure was trusted and the input was untrusted. This was my first important realization: Flask is secure by default. I didn’t need to add filters, sanitizers, or regex. The framework’s design already prevented the vulnerability. I replaced render_template() with render_template_string() and constructed the template using an f-string. This small change is the entire vulnerability. The application now compiles a template at runtime using user data. The server evaluated the expression. The application was no longer a webpage. It had become a remote Python expression interpreter. The page printed Flask configuration values. That means the attacker can access server-side objects, not just HTML output. This is the critical difference between: XSS → affects users SSTI → affects the server Here is what actually happened internally: SSTI is dangerous because the attack does not stay in the web layer. It crosses directly into the application’s execution environment. Individually, these are safe: Combined incorrectly: They form a code execution pipeline. The vulnerability was not caused by a complex algorithm or memory corruption. It was caused by a design decision. Flask encourages safe patterns. Deviating from them introduces risk quickly. Templates must be static. Only variables should be dynamic. It is often a full system compromise entry point. A tool is not insecure by itself. It becomes insecure when used outside its intended model. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: Hello {{ username }} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Hello {{ username }} CODE_BLOCK: Hello {{ username }} CODE_BLOCK: User → sends text Server → prints text Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: User → sends text Server → prints text CODE_BLOCK: User → sends text Server → prints text CODE_BLOCK: User → sends template code Server → executes it Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: User → sends template code Server → executes it CODE_BLOCK: User → sends template code Server → executes it CODE_BLOCK: from flask import Flask, request, render_template app = Flask(__name__) @app.route('/') def index(): return """ <form action="/post" method="GET"> Name: <input type="text" name="name"><br> Message: <input type="text" name="msg"><br> <input type="submit"> </form> """ @app.route('/post') def post(): name = request.args.get('name', 'Anonymous') msg = request.args.get('msg', '') return render_template("message.html", name=name, msg=msg) if __name__ == '__main__': app.run(debug=True) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: from flask import Flask, request, render_template app = Flask(__name__) @app.route('/') def index(): return """ <form action="/post" method="GET"> Name: <input type="text" name="name"><br> Message: <input type="text" name="msg"><br> <input type="submit"> </form> """ @app.route('/post') def post(): name = request.args.get('name', 'Anonymous') msg = request.args.get('msg', '') return render_template("message.html", name=name, msg=msg) if __name__ == '__main__': app.run(debug=True) CODE_BLOCK: from flask import Flask, request, render_template app = Flask(__name__) @app.route('/') def index(): return """ <form action="/post" method="GET"> Name: <input type="text" name="name"><br> Message: <input type="text" name="msg"><br> <input type="submit"> </form> """ @app.route('/post') def post(): name = request.args.get('name', 'Anonymous') msg = request.args.get('msg', '') return render_template("message.html", name=name, msg=msg) if __name__ == '__main__': app.run(debug=True) COMMAND_BLOCK: <!DOCTYPE html> <html> <head><title>Message</title></head> <body> <h1>New Message</h1> <b>{{ name }}</b> says: <p>{{ msg }}</p> </body> </html> Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: <!DOCTYPE html> <html> <head><title>Message</title></head> <body> <h1>New Message</h1> <b>{{ name }}</b> says: <p>{{ msg }}</p> </body> </html> COMMAND_BLOCK: <!DOCTYPE html> <html> <head><title>Message</title></head> <body> <h1>New Message</h1> <b>{{ name }}</b> says: <p>{{ msg }}</p> </body> </html> CODE_BLOCK: /post?name=test&msg={{7*7}} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: /post?name=test&msg={{7*7}} CODE_BLOCK: /post?name=test&msg={{7*7}} CODE_BLOCK: {{7*7}} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: from flask import Flask, request, render_template_string app = Flask(__name__) @app.route('/') def index(): return """ <form action="/post" method="GET"> Name: <input type="text" name="name"><br> Message: <input type="text" name="msg"><br> <input type="submit"> </form> """ @app.route('/post') def post(): name = request.args.get('name', 'Anonymous') msg = request.args.get('msg', '') template = f""" <!DOCTYPE html> <html> <head><title>Message</title></head> <body> <h1>New Message</h1> <b>{name}</b> says: <p>{msg}</p> </body> </html> """ return render_template_string(template) if __name__ == '__main__': app.run(debug=True) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: from flask import Flask, request, render_template_string app = Flask(__name__) @app.route('/') def index(): return """ <form action="/post" method="GET"> Name: <input type="text" name="name"><br> Message: <input type="text" name="msg"><br> <input type="submit"> </form> """ @app.route('/post') def post(): name = request.args.get('name', 'Anonymous') msg = request.args.get('msg', '') template = f""" <!DOCTYPE html> <html> <head><title>Message</title></head> <body> <h1>New Message</h1> <b>{name}</b> says: <p>{msg}</p> </body> </html> """ return render_template_string(template) if __name__ == '__main__': app.run(debug=True) COMMAND_BLOCK: from flask import Flask, request, render_template_string app = Flask(__name__) @app.route('/') def index(): return """ <form action="/post" method="GET"> Name: <input type="text" name="name"><br> Message: <input type="text" name="msg"><br> <input type="submit"> </form> """ @app.route('/post') def post(): name = request.args.get('name', 'Anonymous') msg = request.args.get('msg', '') template = f""" <!DOCTYPE html> <html> <head><title>Message</title></head> <body> <h1>New Message</h1> <b>{name}</b> says: <p>{msg}</p> </body> </html> """ return render_template_string(template) if __name__ == '__main__': app.run(debug=True) CODE_BLOCK: Template + Variables Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Template + Variables CODE_BLOCK: Template + Variables CODE_BLOCK: User Input → Python String → Jinja Template Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: User Input → Python String → Jinja Template CODE_BLOCK: User Input → Python String → Jinja Template CODE_BLOCK: /post?name=test&msg={{7*7}} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: /post?name=test&msg={{7*7}} CODE_BLOCK: /post?name=test&msg={{7*7}} CODE_BLOCK: 49 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: /post?name=test&msg={{config}} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: /post?name=test&msg={{config}} CODE_BLOCK: /post?name=test&msg={{config}} CODE_BLOCK: HTTP Request ↓ Python String ↓ Jinja Template Engine ↓ Python Runtime ↓ Operating System Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: HTTP Request ↓ Python String ↓ Jinja Template Engine ↓ Python Runtime ↓ Operating System CODE_BLOCK: HTTP Request ↓ Python String ↓ Jinja Template Engine ↓ Python Runtime ↓ Operating System - A secure implementation - A vulnerable implementation - Flask routing - request handling - Jinja templating - and most importantly — secure vs unsafe patterns - User input enters Python - Python f-string embeds it - Jinja parses the string - Jinja evaluates expressions - Expressions access Python objects - Python f-strings - Jinja templates