Skip to content

Commit c505fc4

Browse files
committed
feat: add escapeHtml function
This can be used to escape any special characters in a string with HTML before sending from the server back to the client. This is important to prevent a cross-site scripting attack.
1 parent faa896c commit c505fc4

File tree

4 files changed

+67
-2
lines changed

4 files changed

+67
-2
lines changed

src/node/routes/login.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { RateLimiter as Limiter } from "limiter"
44
import * as path from "path"
55
import { rootPath } from "../constants"
66
import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http"
7-
import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString } from "../util"
7+
import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString, escapeHtml } from "../util"
88

99
export enum Cookie {
1010
Key = "key",
@@ -36,6 +36,7 @@ const getRoot = async (req: Request, error?: Error): Promise<string> => {
3636
} else if (req.args.usingEnvHashedPassword) {
3737
passwordMsg = "Password was set from $HASHED_PASSWORD."
3838
}
39+
3940
return replaceTemplates(
4041
req,
4142
content
@@ -111,6 +112,8 @@ router.post("/", async (req, res) => {
111112

112113
throw new Error("Incorrect password")
113114
} catch (error) {
114-
res.send(await getRoot(req, error))
115+
const html = await getRoot(req, error)
116+
const escapedHtml = escapeHtml(html)
117+
res.send(escapedHtml)
115118
}
116119
})

src/node/util.ts

+14
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,17 @@ export const isFile = async (path: string): Promise<boolean> => {
508508
return false
509509
}
510510
}
511+
512+
/**
513+
* Escapes any HTML string special characters, like &, <, >, ", and '.
514+
*
515+
* Source: https://stackoverflow.com/a/6234804/3015595
516+
**/
517+
export function escapeHtml(unsafe: string): string {
518+
return unsafe
519+
.replace(/&/g, "&amp;")
520+
.replace(/</g, "&lt;")
521+
.replace(/>/g, "&gt;")
522+
.replace(/"/g, "&quot;")
523+
.replace(/'/g, "&#039;")
524+
}

test/unit/node/util.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,11 @@ describe("onLine", () => {
445445
expect(await received).toEqual(expected)
446446
})
447447
})
448+
449+
describe("escapeHtml", () => {
450+
it("should escape HTML", () => {
451+
expect(util.escapeHtml(`<div class="error">"Hello & world"</div>`)).toBe(
452+
"&lt;div class=&quot;error&quot;&gt;&quot;Hello &amp; world&quot;&lt;/div&gt;",
453+
)
454+
})
455+
})

test/unit/routes/login.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import * as httpserver from "../../utils/httpserver"
2+
import * as integration from "../../utils/integration"
3+
14
import { RateLimiter } from "../../../src/node/routes/login"
25

36
describe("login", () => {
@@ -34,4 +37,41 @@ describe("login", () => {
3437
expect(limiter.removeToken()).toBe(false)
3538
})
3639
})
40+
describe("/login", () => {
41+
let _codeServer: httpserver.HttpServer | undefined
42+
function codeServer(): httpserver.HttpServer {
43+
if (!_codeServer) {
44+
throw new Error("tried to use code-server before setting it up")
45+
}
46+
return _codeServer
47+
}
48+
49+
// Store whatever might be in here so we can restore it afterward.
50+
// TODO: We should probably pass this as an argument somehow instead of
51+
// manipulating the environment.
52+
const previousEnvPassword = process.env.PASSWORD
53+
54+
beforeEach(async () => {
55+
process.env.PASSWORD = "test"
56+
_codeServer = await integration.setup(["--auth=password"], "")
57+
})
58+
59+
afterEach(() => {
60+
process.env.PASSWORD = previousEnvPassword
61+
})
62+
63+
it("should return escaped HTML with 'Missing password' message", async () => {
64+
const resp = await codeServer().fetch("/login", { method: "POST" })
65+
66+
expect(resp.status).toBe(200)
67+
68+
const htmlContent = await resp.text()
69+
70+
expect(htmlContent).not.toContain(">")
71+
expect(htmlContent).not.toContain("<")
72+
expect(htmlContent).not.toContain('"')
73+
expect(htmlContent).not.toContain("'")
74+
expect(htmlContent).toContain("&lt;div class=&quot;error&quot;&gt;Missing password&lt;/div&gt;")
75+
})
76+
})
3777
})

0 commit comments

Comments
 (0)