Repository: coder/code-server (TypeScript) — Security

Issue:
Cross Site Scripting（XSS）vulnerability in code-server
<!--

Hi there! 👋

Thanks for reporting a bug.

Please search for existing issues before filing, as they may contain additional
information about the problem and descriptions of workarounds. Provide as much
information as you can, so that we can reproduce the issue. Otherwise, we may
not be able to help diagnose the problem, and may close the issue as
unreproducible or incomplete. For visual defects, please include screenshots to
help us understand the issue.
-->

## OS/Web Information

- Web Browser: **firefox**
- Local OS: **Debian**
- Remote OS:**Debian**
- Remote Architecture:
- `code-server --version`: **v3.12.0**

## Steps to Reproduce

1.Open your browser and insert payload `/static/test%3Cmy_tag_efb4535077ba29aaca28167c491b4249/%3E%3Cimg%20src=x%3E%3Cscript%3Ealert(1)%3C/script%3E`

2.example: `http://127.0.0.1:8080/static/test%3Cmy_tag_efb4535077ba29aaca28167c491b4249/%3E%3Cimg%20src=x%3E%3Cscript%3Ealert(1)%3C/script%3E`

<!--
First run code-server with at least debug logging (or trace to be really
thorough) by setting the --log flag or the LOG_LEVEL environment variable. -vvv
and --verbose are aliases for --log trace. For example:

code-server --log debug

Once this is done, replicate the issue you're having then collect logging
information from the following places:

    1. The most recent files from ~/.local/share/code-server/coder-logs.
    2. The browser console.
    3. The browser network tab.

Additionally, collecting core dumps (you may need to enable them first) if
code-server crashes can be helpful.
-->

<!--
If you're having issues with installation please include the installation logs
i.e. the output of `yarn global add code-server` if you installed with `yarn`
 -->

## Screenshot
![image](https://user-images.githubusercontent.com/17903984/137276639-c78d62b4-a91c-467f-a676-50a8abb65fad.png)
<!-- Ideally provide a screenshot, gif, video or screen recording. -->

## Notes

<!-- If you can reproduce the issue on vanilla VS Code,
please file the issue at the VS Code repository instead. -->

This issue can be reproduced in VS Code: Yes



Discussion:
@code-asher is this expected since the person would have to have access to the vm/machine running code-server?
@jsjoeio someone can send you a forged link that makes you execute arbitrary code in your code-server instance. See https://owasp.org/www-community/attacks/xss/
Yeah we should probably make a habit of escaping everything we put into
the HTML or switch to a templating language.

@code-asher in my experience it's better to use a templating language  that escapes everything by default, habits are hard to change ;)
Very true! :D

I believe we have switching to a templating system on our roadmap, maybe
for now we can fix this issue until that switch happens.

@code-asher I can take a crack at the fix
@mauri-sfdc that would be awesome! Thank you for reporting this and providing extra links (for me!). Let us know how we can help.

Resolving patch:
diff --git a/src/node/routes/errors.ts b/src/node/routes/errors.ts
--- a/src/node/routes/errors.ts
+++ b/src/node/routes/errors.ts
@@ -6,7 +6,7 @@ import { WebsocketRequest } from "../../../typings/pluginapi"
 import { HttpCode } from "../../common/http"
 import { rootPath } from "../constants"
 import { replaceTemplates } from "../http"
-import { getMediaMime } from "../util"
+import { escapeHtml, getMediaMime } from "../util"
 
 const notFoundCodes = ["ENOENT", "EISDIR", "FileNotFound"]
 export const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
@@ -29,7 +29,7 @@ export const errorHandler: express.ErrorRequestHandler = async (err, req, res, n
       replaceTemplates(req, content)
         .replace(/{{ERROR_TITLE}}/g, status)
         .replace(/{{ERROR_HEADER}}/g, status)
-        .replace(/{{ERROR_BODY}}/g, err.message),
+        .replace(/{{ERROR_BODY}}/g, escapeHtml(err.message)),
     )
   } else {
     res.json({


Test patch:
diff --git a/test/unit/node/routes/errors.test.ts b/test/unit/node/routes/errors.test.ts
new file mode 100644
--- /dev/null
+++ b/test/unit/node/routes/errors.test.ts
@@ -0,0 +1,35 @@
+import express from "express"
+import { errorHandler } from "../../../../src/node/routes/errors"
+
+describe("error page is rendered for text/html requests", () => {
+  it("escapes any html in the error messages", async () => {
+    const next = jest.fn()
+    const err = {
+      code: "ENOENT",
+      statusCode: 404,
+      message: ";>hello<script>alert(1)</script>",
+    }
+    const req = createRequest()
+    const res = {
+      status: jest.fn().mockReturnValue(this),
+      send: jest.fn().mockReturnValue(this),
+      set: jest.fn().mockReturnValue(this),
+    } as unknown as express.Response
+
+    await errorHandler(err, req, res, next)
+    expect(res.status).toHaveBeenCalledWith(404)
+    expect(res.send).toHaveBeenCalledWith(expect.not.stringContaining("<script>"))
+  })
+})
+
+function createRequest(): express.Request {
+  return {
+    headers: {
+      accept: ["text/html"],
+    },
+    originalUrl: "http://example.com/test",
+    query: {
+      to: "test",
+    },
+  } as unknown as express.Request
+}