{"id":13539238,"url":"https://github.com/dotboris/vuejs-serverside-template-xss","last_synced_at":"2026-01-31T23:45:39.052Z","repository":{"id":146982369,"uuid":"122142395","full_name":"dotboris/vuejs-serverside-template-xss","owner":"dotboris","description":"Demo of a Vue.js app that mixes both clientside templates and serverside templates leading to an XSS vulnerability","archived":false,"fork":false,"pushed_at":"2019-09-11T01:26:45.000Z","size":18,"stargazers_count":300,"open_issues_count":4,"forks_count":22,"subscribers_count":13,"default_branch":"master","last_synced_at":"2024-11-03T04:32:21.252Z","etag":null,"topics":["security","vue","vue2","vuejs","vuejs2","xss","xss-vulnerability"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dotboris.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-02-20T01:25:09.000Z","updated_at":"2024-10-11T11:43:21.000Z","dependencies_parsed_at":"2024-01-16T15:43:33.749Z","dependency_job_id":"db7ce899-1d64-4809-9412-86b2977fe067","html_url":"https://github.com/dotboris/vuejs-serverside-template-xss","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotboris%2Fvuejs-serverside-template-xss","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotboris%2Fvuejs-serverside-template-xss/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotboris%2Fvuejs-serverside-template-xss/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotboris%2Fvuejs-serverside-template-xss/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dotboris","download_url":"https://codeload.github.com/dotboris/vuejs-serverside-template-xss/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246767560,"owners_count":20830512,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["security","vue","vue2","vuejs","vuejs2","xss","xss-vulnerability"],"created_at":"2024-08-01T09:01:22.228Z","updated_at":"2026-01-31T23:45:39.020Z","avatar_url":"https://github.com/dotboris.png","language":"PHP","funding_links":[],"categories":["PHP","PHP (184)","\u003ca id=\"683b645c2162a1fce5f24ac2abfa1973\"\u003e\u003c/a\u003e漏洞\u0026\u0026漏洞管理\u0026\u0026漏洞发现/挖掘\u0026\u0026漏洞开发\u0026\u0026漏洞利用\u0026\u0026Fuzzing"],"sub_categories":["\u003ca id=\"5d7191f01544a12bdaf1315c3e986dff\"\u003e\u003c/a\u003eXSS\u0026\u0026XXE"],"readme":"# Vue.js serverside template XSS example\n\nThis repository demonstrates how web apps that use both serverside rendering\nand Vue.js can be vulnerable to XSS even if they take precautions.\n\n`index.php` is a vulnerable PHP script. `fix-v-pre.php` and\n`fix-servervars-global.php` are fixed versions of the vulnerable script.\nThe rest of this README walks through how to exploit the vulnerability, shows how\nto fix the vulnerability and then discusses the scope and impact of such a\nvulnerability.\n\nNote that this vulnerability is not specific to PHP nor is it specific to Vue.js.\nIf you have an app that mixes serverside rendering with clientside rendering,\nyou might be vulnerable.\n\nI suggest that you run the demo, try to exploit it and then try to fix it. It's\na great learning experience.\n\n## Running the demo\n\n1.  Install [docker \u0026 docker-compose](https://docs.docker.com/install/)\n1.  Run the demo\n\n    ```sh\n    docker-compose up -d\n    ```\n\n1.  Open \u003chttp://localhost:8080\u003e in your browser\n\nIf you don't want to bother with `docker` you can throw the `index.php` file on\na PHP capable server and host it there. Keep in mind that this file is\nvulnerable to XSS by design, you should run this on a local environment.\n\n## Walkthrough\n\n:rotating_light: Warning: spoilers ahead! If you want to exploit this\nyourself, stop reading. The following gives away the solution. :rotating_light:\n\nFirst we open the app and see that we have two things to play with:\n\n1.  a textbox that lets you inject text\n1.  a counter application\n\nWhen we play around with the textbox, see that whatever we put in there ends up\nas a query parameter. If we put `foobar` in the textbox, we end up with the\nfollowing url: `http://localhost:8080/?injectme=foobar`. We can also see that\nthe text `foobar` is injected in the page.\n\nThis injection looks to be done serverside. We can confirm this by looking at\nthe source of the page. We see that `foobar` was part of the response sent by\nthe server. We have an opportunity for XSS here. Let's try the usual bag of\ntricks.\n\nWhen we try `\u003cscript\u003ealert('xss')\u003c/script\u003e` we get\n`\u0026lt;script\u0026gt;alert('xss')\u0026lt;/script\u0026gt;`.\nSimilarly, if we try `\u003cimg src=\"nope.jpg\" onerror=\"alert('xss')\"/\u003e` we get\n`\u0026lt;img src=\u0026quot;nope.jpg\u0026quot; onerror=\u0026quot;alert('xss')\u0026quot;/\u0026gt;`.\n\nLooks like everything gets escaped properly. We can confirm this by looking at\nthe source code of the page. Back to the drawing board. Let's try to understand\nhow this app works. How does the counter app work? Maybe it can help us here.\n\nThe counter app is built with Vue.js. For those who are not familiar, Vue.js is\na javascript lib that runs it the browser. It lets you build dynamic frontend\napps.\n\nOne way of using Vue.js is to write a template in the HTML of your page and then\ntell Vue.js to render it through javascript. This is a common thing to do when\nyou have an application that uses serverside rendering and you want to add some\ndynamism to it. This is exactly what's happening here.\n\nWhen we look at the template, we see that our injected value from before is\nrendered directly inside of the template.\n\n```html\n\u003cdiv id=\"injectable-app\"\u003e\n  \u003cdiv\u003e\n    You have injected: OUR_INJECTED_VALUE_GOES_HERE\n  \u003c/div\u003e\n\n  \u003cbutton type=\"button\" @click=\"dec\"\u003e-\u003c/button\u003e\n  {{counter}}\n  \u003cbutton type=\"button\" @click=\"inc\"\u003e+\u003c/button\u003e\n\u003c/div\u003e\n```\n\nMaybe we can exploit this. Vue.js templates allow you to use expressions.\nExpressions are bits of code that take some data, transform it and output it.\nThey're basically javascript. In Vue.js expressions have the form\n`{{ ... code goes here ... }}`.\n\nLet's give this a shot. We write `{{ 2 + 2 }}` in the textbox and we get\n`You have injected: 4`. It works! :tada:\n\nFor good measure we'll do an `alert` to prove that we have full control. We put\n`{{ alert('xss') }}` in the textbox and nothing. The whole counter is gone. When\nwe look at the console we get an error:\n\n```\nTypeError: alert is not a function\n    at Proxy.eval (eval at createFunction (vue.js:10518), \u003canonymous\u003e:2:114)\n    at Vue$3.Vue._render (vue.js:4465)\n    at Vue$3.updateComponent (vue.js:2765)\n    at Watcher.get (vue.js:3113)\n    at new Watcher (vue.js:3102)\n    at mountComponent (vue.js:2772)\n    at Vue$3.$mount (vue.js:8416)\n    at Vue$3.$mount (vue.js:10777)\n    at Vue$3.Vue._init (vue.js:4557)\n    at new Vue$3 (vue.js:4646)\n```\n\n`alert` is not a function? What's going on here? Vue.js expressions are\nevaluated in the context of the `Vue` instance that they are rendered with.\nIn other words, when we try to render `{{ foobar }}`, it looks for the `foobar`\nproperty in the template data. When we wrote `{{ alert('xss') }}` it was the\ninterpreted as `templateData.alert('xss')`. We got the error because our\ntemplate data does not have a property named `alert`.\n\nYou can think of this as being stuck inside a javascript jail or sandbox. It's\nimportant to note that Vue.js doesn't have a real sandbox. It doesn't actively\ntry to prevent you from accessing stuff outside the template data. This is just\na side effect of how it evaluates expressions.\n\nHow do we get out of this \"sandbox\"? There are many ways. If you want to flex\nyour javascript muscles you can give it a shot. The solution I went with is:\n\n```\n{{ constructor.constructor(\"alert('xss')\")() }}\n```\n\nThis looks obtuse but it's surprisingly simple. We know that we're evaluated\nagainst our template data. When we write `constructor`, it's interpreted as\n`templateData.constructor`. Our template data is an object. All objects in\njavascript have a constructor. So `constructor` gives us `Vue$3` (the Vue.js\nconstructor).\n\nIn javascript, all constructors are functions and all functions are objects.\nThis means that `Vue$3` has a constructor. This constructor is the `Function`\nconstructor. Writing `constructor.constructor` gives us the `Function`\nconstructor.\n\nThe `Function` constructor let's us define a function dynamically at runtime. We\npass it the code of our function and it returns a function that we can run. In\nthis case we end up with `Function(\"alert('xss')\")()`. This creates a function\nthat calls `alert` (the real `alert` in the global scope) and then calls it.\n\nThat's it. We've made it, we have injected javascript in a page we don't control\nand this javascript has access to the global scope. At this point we can do\nanything the browser can. This is full blown XSS.\n\n## Why does this work?\n\nThis exploit is possible because the app is mixing serverside rendering and\nclientside rendering.\n\nIn this case, we have our PHP app that takes user input (a query parameter) and\nuses it to render an HTML page. The app escapes the input for HTML entities\nensuring that simple XSS is impossible. When the page makes it into the browser,\nVue.js takes part of this HTML and renders it like a template. We've seen that\nit's basically doing a complex `eval` on that HTML.\n\nIn this context, Vue.js can't tell the difference between the user input which\nmay not be safe and the template which is essentially code and is considered\nsafe.\n\nWhen it's able to tell the difference between user input and template code,\nVue.js does an amazing job at preventing XSS. In fact, it does a better job than\nPHP because it will treat it's template data as dangerous by default and always\nescape it. You don't have to remember to escape your data.\n\n## How can I protect against this?\n\nThe simple fix for this is to use the\n[`v-pre` directive](https://vuejs.org/v2/api/#v-pre) whenever you are injecting\nserverside values into a clientside template.\n\nIn this case, you would change\n\n```html\n\u003cdiv id=\"injectable-app\"\u003e\n  \u003cdiv\u003e\n    You have injected:\n    \u003c?= htmlspecialchars($_GET['injectme']) ?\u003e\n  \u003c/div\u003e\n\n  \u003cbutton type=\"button\" @click=\"dec\"\u003e-\u003c/button\u003e\n  {{counter}}\n  \u003cbutton type=\"button\" @click=\"inc\"\u003e+\u003c/button\u003e\n\u003c/div\u003e\n```\n\nto\n\n```html\n\u003cdiv id=\"injectable-app\"\u003e\n  \u003cdiv v-pre\u003e\n    You have injected:\n    \u003c?= htmlspecialchars($_GET['injectme']) ?\u003e\n  \u003c/div\u003e\n\n  \u003cbutton type=\"button\" @click=\"dec\"\u003e-\u003c/button\u003e\n  {{counter}}\n  \u003cbutton type=\"button\" @click=\"inc\"\u003e+\u003c/button\u003e\n\u003c/div\u003e\n```\n\nWhile this solution does work, it's not great. It's easy for anyone to forget\nto use the `v-pre` directive. If a single developer forgets to do this, you're\nscrewed all over again.\n\nWhen it comes to security, I prefer systematic solutions. A better solution\nwould be to define a global variable in the page that holds all serverside\nvariables. This does not prevent a developer from mixing serverside and\nclientside rendering but it does give them a secure mechanism for passing values\nfrom the server to the client.\n\nWe can implement this like so:\n\n```html\n\u003cdiv id=\"injectable-app\"\u003e\n  \u003cdiv\u003e\n    You have injected: {{ SERVER_VARS.injectMe }}\n  \u003c/div\u003e\n\n  \u003cbutton type=\"button\" @click=\"dec\"\u003e-\u003c/button\u003e\n  {{counter}}\n  \u003cbutton type=\"button\" @click=\"inc\"\u003e+\u003c/button\u003e\n\u003c/div\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue.js\"\u003e\u003c/script\u003e\n\u003c?php\n$serverVars = [\n  'injectMe' =\u003e (string) $_GET['injectme']\n];\n?\u003e\n\u003cscript\u003e\nwindow.SERVER_VARS = JSON.parse(atob('\u003c?= base64_encode(json_encode($serverVars)) ?\u003e'));\nVue.prototype.SERVER_VARS = window.SERVER_VARS;\n\u003c/script\u003e\n```\n\nThe full fix is available in `fix-servervars-global.php`.\n\n## Is this a real threat?\n\nAfter reading this, you might wonder: Why would anyone in their right minds mix\nseverside and clientside rendering?\n\nI think that it's pretty reasonable for a developer to add Vue.js to their\nexisting serverside rendering app and think that everything is going to be fine.\nVue.js advertises itself as a \"progressive frameworks\". They expect you to do\nthis. Also, the security risks are not immediately apparent. Getting our XSS to\nrun was pretty roundabout.\n\nIf you do a little googling, you'll find a bunch of examples and tutorials on\nhow to use Vue.js with other serverside rendering frameworks. While I don't have\nthe numbers to back this, I think that there are plenty of apps out there that\nmix serverside rendering and clientside rendering. All of those apps could be\nvulnerable to this kind of XSS.\n\n## Is this specific to PHP?\n\nNot at all. This can work with any serverside language or technology. You can\ntake this example and rewrite it in any serverside technology and it would still\nbe vulnerable. It doesn't matter how much automatic escaping this technology\ndoes, because no one automatically escapes variables injected in Vue.js\ntemplates.\n\n## What about other frameworks / libs?\n\nAny library or framework that lets you write templates in HTML is potentially\nvulnerable to this.\n\nAngular 1 apps are famously vulnerable to this. In their\n[security guide](https://docs.angularjs.org/guide/security#angularjs-templates-and-expressions)\nthe angular team warns explicitly about this.\n\n\u003e Generating AngularJS templates on the server containing user-provided content.\n\u003e This is the most common pitfall where you are generating HTML via some\n\u003e server-side engine such as PHP, Java or ASP.NET.\n\nThey've also tried for a long time to build a sandbox that mitigates against XSS\ncoming from the server. Every time that they've improved or fixed the sandbox,\nit was broken or bypassed. Eventually the angular team\n[got rid of the sandbox](https://docs.angularjs.org/guide/security#sandbox-removal).\n\nFrameworks like React and Angular 2+ are not likely to be vulnerable to this\nkind of attack because they don't let you write templates in HTML and they force\nthe use of a compiler. This makes injecting user input from the server into\nclientside templates very unlikely. I'm not saying that React and Angualar 2+\nare bullet proof. You just have to try harder to be vulnerable.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdotboris%2Fvuejs-serverside-template-xss","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdotboris%2Fvuejs-serverside-template-xss","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdotboris%2Fvuejs-serverside-template-xss/lists"}