Server-side template injection happens when an application builds a template on the server and lets untrusted input become part of the template source, not just a value the template prints. The server then evaluates that source. A browser never has to run it. That is the opposite of cross-site scripting, where the victim’s browser executes a script. Both bugs start from mixing data with a language. They end in different places, and the fix is not the same header.
Command injection, when the server passes input to a shell, is the nearer cousin. Read command injection in web apps for that boundary. This page stays on templates. It does not list engine syntax. One illustration is enough.
What the server did, in one harmless picture
A template’s job is to fill blanks. The programmer writes a fixed layout and passes a name, a total, or a date. The safe blank is a value. The unsafe blank is a piece of the template itself. If a visitor can change the template text, the engine may evaluate an expression they supplied.
Picture a page that should greet someone by a name they typed. The safe version inserts the characters of the name into a fixed sentence. The unsafe version concatenates the name into the template source and asks the engine to render that new source. If the name is actually an expression such as seven times seven, a vulnerable engine prints 49. Nothing was installed on the visitor’s computer. The server did arithmetic because it treated the input as code. Real attacks ask the engine for files or commands. You do not need those examples to recognize the bug, and this article will not provide them.
Where it shows up
Teams meet it in email builders, PDF invoices, CMS themes that evaluate a snippet, error pages that echo a template path, and admin tools that preview a message “as the user typed it.” The feature sounds helpful. The implementation passes the typed string to the same function that renders trusted theme files. Preview features are a common sink because they exist specifically to render what someone typed.
Client-side template libraries in the browser are a different product. If the expression runs only in the page, you are in the XSS family, including the DOM cases covered by output encoding and XSS prevention for web forms. A content security policy does not stop the server from evaluating a template before the HTML is sent. Headers are still worth setting. They are not this fix.
The fix is to stop rendering input as a template
Pass user data as values into a template you wrote and shipped. Do not concatenate input into the template string. Do not offer a “custom template” field unless that field is a sandboxed language you are prepared to maintain, which most product teams are not. If a designer needs conditionals, give them a fixed set of layouts and let them choose a layout id. The id maps to a file on disk. The visitor never supplies the file.
Sandboxes inside template engines are easy to get wrong and tend to gain features until they are dangerous again. Prefer not having the sink. When a library is only used to render your own files, keep user content out of the render call’s source argument. Review every place the code calls render, compile, or parse on a string that began in a request, a database row a user wrote, or a file a user uploaded.
What a Vulnify scan actually checks
Do not read a Standard or Comprehensive website scan as a template-safety test. The product text has disagreed with itself about a homepage parameter, and the scan code that ships does not implement a general homepage template check on those depths. What does exist is narrower.
The aggressive active probes add-on can send a harmless arithmetic canary on query parameter names discovered by a short crawl. It runs only on a hostname you have verified. A hit means the response contained the computed result and the baseline response did not. A miss means that canary was not evaluated on those parameters. It does not mean every template path was reviewed. A pentest can also try a small set of URLs that already have a query string, with the same idea: if the server prints the result of a simple expression, the check fails. URLs with no query string are not that check.
A clean report from a scan that did not run those probes is silence, not a pass. Say so in the ticket. How to read a website vulnerability scan report is the same kind of reading skill for a different limit.
How to confirm a fix
Find the render call in code review and show that user input is now a value, or that the feature no longer renders user-supplied template source. Deploy that change. Repeat the same request shape the finding used, on a host you are allowed to test, and confirm the expression comes back as text or is rejected, rather than as a computed number. Then rerun the same add-on or pentest check if that is what produced the original finding. A different scan depth that never sent the canary cannot close the ticket.
SQL injection is another case of a backend treating input as query structure. The sink is a database query, not a template. Do not file them as one bug because both are “injection.”
Search the code for the render call
Search for the template engine's render, compile, or parse function and read each caller. The dangerous caller is the one that builds a string from a request, a stored comment, a theme option, or an uploaded file, then renders that string. The safe caller loads a file path the developer chose. You want a short list of callers in the ticket, with the dangerous ones marked fixed. A dependency scanner that flags an old template library is a different task. Patching the library does not remove a feature that renders visitor input.
If the preview feature must stay, give it sample data only, from a fixed template id. Editors can preview a real customer's name as a value. They should not preview a template they pasted. That requirement is a product decision. Write it down so the next redesign does not restore the text box.
When the ticket closes, link the code change and the rerun of the same probe. A different scan that never sent a canary is not the verification, even if its grade looks better. Leave a one-line comment on the render function that user strings are values, so the next feature does not concatenate a preview box back into the template source.
Frequently asked questions
Is this XSS?
No. XSS runs in the browser. Server-side template injection runs while the server builds the response. A page can have both. Fixing one does not fix the other.
Does a passing website scan clear my templates?
No. Template canaries are not part of a normal depth. They belong to aggressive probes on a verified host, and to a narrow pentest check on URLs that already have parameters.
Should I disable my template engine?
Keep it for files you authored. Stop passing visitor-controlled strings to it as source. Choosing a layout by id is the feature most preview screens actually needed.
