Skip to content

Commit bc09c09

Browse files
committed
Add CSRF to login form; format files
1 parent 24cb393 commit bc09c09

15 files changed

+695
-722
lines changed

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ proto:
2020
format:
2121
@goimports -w -l $(GO_SOURCES)
2222

23+
format-web:
24+
cd pkg/web/ && npm run format
25+
2326
.PHONY: format-check
2427
format-check:
2528
@out=$$(goimports -l $(GO_SOURCES)) && echo "$$out" && test -z "$$out"

k6/internal/01.quickpizza.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import chai, {
66
chai.config.exitOnError = true;
77

88
const BASE_URL = __ENV.BASE_URL || "http://localhost:3333";
9+
const CSRF = "NTQyNjg1OTc2"
910

1011
export const options = {
1112
vus: 1,
@@ -26,7 +27,7 @@ function randomString(length) {
2627

2728
function testDatabaseCreatedUserLogin() {
2829
describe("Log in as a user that was already inserted in the DB", () => {
29-
let data = {username: "synthetics_multihttp_example", password: "synthetics_multihttp_example"};
30+
let data = {username: "synthetics_multihttp_example", password: "synthetics_multihttp_example", csrf: CSRF};
3031
var res = http.post(`${BASE_URL}/api/users/token/login`, JSON.stringify(data), {
3132
headers: {
3233
'Content-Type': 'application/json',
@@ -52,6 +53,7 @@ function testCreateUserLogin() {
5253

5354
expect(res.status, "response status").to.equal(201);
5455

56+
data.csrf = CSRF;
5557
res = http.post(`${BASE_URL}/api/users/token/login`, JSON.stringify(data), {
5658
headers: {
5759
'Content-Type': 'application/json',
@@ -66,7 +68,8 @@ function testCreateUserLogin() {
6668
// Invalid password
6769
var res = http.post(`${BASE_URL}/api/users/token/login`, JSON.stringify({
6870
username: username,
69-
password: "foo",
71+
password: "foo",
72+
csrf: CSRF,
7073
}), {
7174
headers: {
7275
'Content-Type': 'application/json',
@@ -80,6 +83,7 @@ function testCreateUserLogin() {
8083
res = http.post(`${BASE_URL}/api/users/token/login`, JSON.stringify({
8184
username: "foo",
8285
password: "foo",
86+
csrf: CSRF,
8387
}), {
8488
headers: {
8589
'Content-Type': 'application/json',
@@ -92,6 +96,7 @@ function testCreateUserLogin() {
9296
res = http.post(`${BASE_URL}/api/users/token/login`, JSON.stringify({
9397
username: "default",
9498
password: "foobar",
99+
csrf: CSRF,
95100
}), {
96101
headers: {
97102
'Content-Type': 'application/json',

pkg/http/http.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ const (
144144
authHeader = "Authorization"
145145
cookieName = "qp_user_token"
146146
piDecimals = "1415926535897932384626433832795028841971693993751058209749445923078164"
147+
148+
// This should be generated dynamically instead.
149+
// For simplicity we have one global one, because for demonstration purposes
150+
// this is good enough. Note that this is also hardcoded in the login form's HTML.
151+
serverCSRFToken = "NTQyNjg1OTc2"
147152
)
148153

149154
var authError = errors.New("authentication failed")
@@ -416,7 +421,7 @@ func (s *Server) AddTestK6IO() {
416421
"/flip_coin.php": "test.k6.io/flip_coin.html",
417422
"/browser.php": "test.k6.io/browser.html",
418423
"/my_messages.php": "test.k6.io/my_messages.html",
419-
"/admin.php": "test.k6.io/admin.html",
424+
"/admin.php": "test.k6.io/admin.html",
420425
}
421426

422427
s.router.Group(func(r chi.Router) {
@@ -878,15 +883,21 @@ func (s *Server) AddCatalogHandler(db *database.Catalog) {
878883
// token, and return the user token (if credentials are valid).
879884
r.Post("/api/users/token/login", func(w http.ResponseWriter, r *http.Request) {
880885
type loginData struct {
881-
Username string `json:"username"`
882-
Password string `json:"password"`
886+
Username string `json:"username"`
887+
Password string `json:"password"`
888+
CSRFToken string `json:"csrf"`
883889
}
884890
var data loginData
885891

886892
if s.decodeJSONBody(w, r, &data) != nil {
887893
return
888894
}
889895

896+
if data.CSRFToken != serverCSRFToken {
897+
s.writeJSONErrorResponse(w, r, errors.New("invalid csrf token"), http.StatusUnauthorized)
898+
return
899+
}
900+
890901
user, err := db.LoginUser(r.Context(), data.Username, data.Password)
891902
if err != nil {
892903
s.log.ErrorContext(r.Context(), "Failed to login user", "err", err)

pkg/web/src/routes/login/+page.svelte

+6-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
async function handleSubmit() {
2525
const res = await fetch(`${PUBLIC_BACKEND_ENDPOINT}api/users/token/login?set_cookie=1`, {
2626
method: 'POST',
27-
body: JSON.stringify({ username: username, password: password }),
27+
body: JSON.stringify({
28+
username: username,
29+
password: password,
30+
csrf: document.getElementById('csrf-token').value
31+
}),
2832
credentials: 'same-origin'
2933
});
3034
if (!res.ok) {
@@ -112,6 +116,7 @@
112116
QuickPizza User Login
113117
</h1>
114118
<form class="space-y-4 md:space-y-6" on:submit|preventDefault={handleSubmit}>
119+
<input type="hidden" name="csrftoken" id="csrf-token" value="NTQyNjg1OTc2" />
115120
<div>
116121
<label for="text" class="block mb-2 text-sm font-medium text-gray-900"
117122
>Username (hint: default)</label

pkg/web/test.k6.io/admin.html

+38-36
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,42 @@
1-
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
1+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
22
<html>
3-
<head>
4-
<title>My messages</title>
5-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
6-
<link rel="icon" href="/test.k6.io/static/favicon.ico" sizes="32x32">
7-
</head>
8-
<body>
3+
<head>
4+
<title>My messages</title>
5+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
6+
<link rel="icon" href="/test.k6.io/static/favicon.ico" sizes="32x32" />
7+
</head>
8+
<body>
9+
<p><a href="/test.k6.io/">&lt; Back</a></p>
910

10-
<p><a href="/test.k6.io/">&lt; Back</a></p>
11+
<h2>Welcome, admin!</h2>
1112

12-
<h2>Welcome, admin!</h2>
13-
14-
<table cellpadding="3" cellspacing="0" border="1" width="50%">
15-
<tr>
16-
<td width="1%"><b>#</b></td><td><b>From:</b></td><td><b>Subject:</b></td>
17-
</tr>
18-
<tr>
19-
<tr>
20-
<td>1</td>
21-
<td>DenyHosts</td>
22-
<td>DenyHosts report on test.k6.io</td>
23-
</tr><tr>
24-
<td>2</td>
25-
<td>Twitter</td>
26-
<td>Grafana is now following you on Mastodon</td>
27-
</tr><tr>
28-
<td>3</td>
29-
<td>Mail Delivery Subsystem</td>
30-
<td>Delivery Status Notification (Failure)</td>
31-
</tr></tr>
32-
</table>
33-
<br>
34-
<form method="POST" action="/my_messages.php">
35-
<input type="hidden" name="redir" value="1">
36-
<input type="hidden" name="csrftoken" value="c3p8Mjtu1K7EbYTm">
37-
<input type="submit" value="Logout">
38-
</form>
39-
</body>
13+
<table cellpadding="3" cellspacing="0" border="1" width="50%">
14+
<tr>
15+
<td width="1%"><b>#</b></td>
16+
<td><b>From:</b></td>
17+
<td><b>Subject:</b></td>
18+
</tr>
19+
<tr>
20+
<td>1</td>
21+
<td>DenyHosts</td>
22+
<td>DenyHosts report on test.k6.io</td>
23+
</tr>
24+
<tr>
25+
<td>2</td>
26+
<td>Twitter</td>
27+
<td>Grafana is now following you on Mastodon</td>
28+
</tr>
29+
<tr>
30+
<td>3</td>
31+
<td>Mail Delivery Subsystem</td>
32+
<td>Delivery Status Notification (Failure)</td>
33+
</tr>
34+
</table>
35+
<br />
36+
<form method="POST" action="/my_messages.php">
37+
<input type="hidden" name="redir" value="1" />
38+
<input type="hidden" name="csrftoken" value="c3p8Mjtu1K7EbYTm" />
39+
<input type="submit" value="Logout" />
40+
</form>
41+
</body>
4042
</html>

0 commit comments

Comments
 (0)