1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
|
local test_directory = arg[0]:match("^(.*)/") or "."
local h = dofile(test_directory .. "/harness.lua")
local script = arg[1]
local variant = arg[2]
local directory = assert(arg[3], "test-auth.lua needs a directory for its fixtures")
local real_hash = arg[4]
if real_hash == nil then
h.stub_auth_modules()
end
local password = "open sesame"
local stored_hash = real_hash or "$6$rounds=300000$testsalt$" .. password
if variant == "file" then
local users_file = io.open(directory .. "/auth-users", "w")
users_file:write("Alice:" .. stored_hash .. "\r\n")
users_file:write("not a parsable line\n")
users_file:write("carol:" .. stored_hash .. "\n")
users_file:close()
local groups_file = io.open(directory .. "/auth-groups", "w")
groups_file:write("DEVS:Alice, bob\n")
groups_file:write("others:carol, alice\n")
groups_file:close()
local repos_file = io.open(directory .. "/auth-repos", "w")
repos_file:write("secret-repo:Devs\n")
repos_file:write("both-repo:devs,others\n")
repos_file:write("empty-repo:ghosts\n")
repos_file:write("bare-repo:\n")
repos_file:close()
h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users")
h.redirect_file("/etc/cgit-auth/groups", directory .. "/auth-groups")
h.redirect_file("/etc/cgit-auth/repos", directory .. "/auth-repos")
end
local inline_config = {
{ "local protected_repos = {\n", "local protected_repos = {\n" ..
"\t[\"secret-repo\"] = { alice = true, Bob = true },\n" ..
"\t[\"both-repo\"] = { alice = true, bob = true, carol = true },\n" ..
"\t[\"empty-repo\"] = {},\n" },
{ "local users = {\n", "local users = {\n" ..
"\tAlice = \"" .. stored_hash .. "\",\n" ..
"\tcarol = \"" .. stored_hash .. "\",\n" },
}
local function load_script(changes, secret_path)
local all = {
{ "local secret_filename = \"/var/lib/cgit/auth-secret\"",
"local secret_filename = " .. string.format("%q", secret_path or directory .. "/auth-secret") },
}
if variant == "inline" then
for _, change in ipairs(inline_config) do
all[#all + 1] = change
end
end
for _, change in ipairs(changes or {}) do
all[#all + 1] = change
end
h.load_with(script, all)
end
load_script()
local session_name = "__Host-cgitauth"
local unistd = require("posix.unistd")
local real_crypt = unistd.crypt
local crypt_settings = {}
unistd.crypt = function(trypass, setting)
crypt_settings[#crypt_settings + 1] = setting
return real_crypt(trypass, setting)
end
local function read_file(path)
local file = io.open(path, "rb")
if file == nil then
return nil
end
local text = file:read("*a")
file:close()
return text
end
local function write_file(path, text)
local file = assert(io.open(path, "wb"))
file:write(text)
file:close()
end
local sysstat = require("posix.sys.stat")
local real_umask = sysstat.umask
local umasks = {}
sysstat.umask = function(mask)
local old = real_umask(mask)
umasks[#umasks + 1] = { mask = mask, old = old }
return old
end
local real_link = unistd.link
local linked = {}
local race_winner = nil
unistd.link = function(from, to)
linked[#linked + 1] = from
if race_winner then
write_file(to, race_winner)
end
return real_link(from, to)
end
local _, made = pcall(get_secret)
h.check("a new secret is 64 hex characters",
type(made) == "string" and #made == 64 and not made:find("%X"), tostring(made))
h.equals("and is what its file holds", read_file(directory .. "/auth-secret"), made)
if real_hash then
local stat = sysstat.stat(directory .. "/auth-secret")
h.equals("which only its owner can read", stat and stat.st_mode % 512, 384)
end
h.equals("it is written under a mask that keeps everyone else out", umasks[1] and umasks[1].mask, 63)
h.check("and the mask before it goes back after",
#umasks == 2 and umasks[2].mask == umasks[1].old)
h.equals("it reaches its name through one link", #linked, 1)
h.equals("and the temporary file it was linked from is removed", linked[1] and read_file(linked[1]), nil)
load_script()
h.equals("a later load takes the same secret from the file", select(2, pcall(get_secret)), made)
h.equals("without making another", #linked, 1)
race_winner = string.rep("9", 64)
load_script(nil, directory .. "/raced-secret")
h.equals("a secret made meanwhile by another request wins", select(2, pcall(get_secret)), race_winner)
h.equals("and is left as it was", read_file(directory .. "/raced-secret"), race_winner)
h.equals("while the temporary file is still removed", read_file(linked[#linked]), nil)
race_winner = nil
write_file(directory .. "/typed-secret", made .. "\n")
load_script(nil, directory .. "/typed-secret")
h.equals("a secret ending in a newline is read without it", select(2, pcall(get_secret)), made)
write_file(directory .. "/short-secret", "0123456789\n")
load_script(nil, directory .. "/short-secret")
local short_ok, short_err = pcall(get_secret)
h.check("a secret of the wrong length is refused", not short_ok)
h.contains("as malformed", tostring(short_err), "is malformed, expected 64 hex characters")
load_script()
local hash = account_hash("ALICE")
h.equals("an account is found whatever its case", hash, stored_hash)
h.equals("an unknown account is not", account_hash("nobody"), nil)
local _, stand_in = account_hash("nobody")
h.equals("it brings another account's hash to spend instead", stand_in, stored_hash)
local userset = repo_userset("secret-repo")
h.check("a protected repository lists its users",
userset ~= nil and userset.alice and userset.bob)
h.equals("an unlisted repository is public", repo_userset("unlisted"), nil)
h.equals("the repository name matches case exactly", repo_userset("Secret-Repo"), nil)
local empty = repo_userset("empty-repo")
h.check("a protected repository with no members denies as an empty set",
empty ~= nil and next(empty) == nil)
local both = repo_userset("both-repo")
h.check("a repository of two groups lets in the members of each",
both ~= nil and both.alice and both.bob and both.carol)
if variant == "file" then
local bare = repo_userset("bare-repo")
h.check("a protected repository with no groups at all denies too",
bare ~= nil and next(bare) == nil)
end
if variant == "file" then
h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users-missing")
h.equals("a missing users file turns every login down", account_hash("alice"), nil)
h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users")
h.redirect_file("/etc/cgit-auth/repos", directory .. "/auth-repos-missing")
h.reset()
local refused = repo_userset("unlisted")
h.check("a missing repos file refuses every repository",
refused ~= nil and next(refused) == nil)
h.contains("and says so in the error log", h.error_log(), "[cgit] Unable to read the auth repos file: ")
local index = repo_userset("")
h.check("and the index as well", index ~= nil and next(index) == nil)
h.redirect_file("/etc/cgit-auth/repos", directory .. "/auth-repos")
h.redirect_file("/etc/cgit-auth/groups", directory .. "/auth-groups-missing")
local memberless = repo_userset("secret-repo")
h.check("a missing groups file leaves a protected repository with nobody let in",
memberless ~= nil and next(memberless) == nil)
h.redirect_file("/etc/cgit-auth/groups", directory .. "/auth-groups")
end
h.equals("decode handles escapes and plus", url_decode("%41+b"), "A b")
h.equals("encode escapes what is not a word", url_encode("a b|c"), "a+b%7Cc")
local tricky = "x&=?|"
h.equals("decode inverts encode", url_decode(url_encode(tricky)), tricky)
local params = parse_query("u=a&p=b=c&x=%41")
h.equals("a query splits on ampersands", params.u, "a")
h.equals("a value keeps its own equals signs", params.p, "b=c")
h.equals("a value is decoded", params.x, "A")
h.equals("a cookie is found among others",
get_cookie("foo=1; cgitauth=abc; bar=2", "cgitauth"), "abc")
h.equals("a lone cookie is found", get_cookie("cgitauth=abc", "cgitauth"), "abc")
h.equals("no header yields no cookie", get_cookie(nil, "cgitauth"), nil)
h.equals("a longer name does not match", get_cookie("xcgitauth=z", "cgitauth"), nil)
h.equals("a magic character in the name is taken literally", get_cookie("a-b=z", "a-b"), "z")
h.equals("spaces around a pair are ignored", get_cookie(" foo=1 ; cgitauth=abc ", "cgitauth"), "abc")
h.equals("a cookie value keeps its own equals signs", get_cookie("cgitauth=a=b", "cgitauth"), "a=b")
local spaces = string.rep(" ", 100000)
local started = os.clock()
get_cookie("a=" .. spaces .. "b", "cgitauth")
h.check("a long run of spaces inside a cookie is read within half a second", os.clock() - started < 0.5)
started = os.clock()
local found = get_cookie(spaces .. "cgitauth=abc", "cgitauth")
h.check("and one ahead of it as well", os.clock() - started < 0.5)
h.equals("without losing the cookie behind it", found, "abc")
h.check("equal strings compare equal", constant_equals("abc", "abc"))
h.check("differing strings do not", not constant_equals("abc", "abd"))
h.check("nor do strings whose byte differences cancel out",
not constant_equals("ab", "ba") and not constant_equals("ac", "bb"))
h.check("differing lengths do not", not constant_equals("a", "ab"))
h.check("non strings do not", not constant_equals(nil, "a"))
h.check("a local path is a safe redirect", is_safe_redirect("/x"))
h.check("and so is the root", is_safe_redirect("/"))
h.check("a scheme relative target is not", not is_safe_redirect("//evil"))
h.check("a backslash variant is not", not is_safe_redirect("/\\evil"))
h.check("nor one of backslashes alone", not is_safe_redirect("\\\\evil"))
h.check("nor an absolute url", not is_safe_redirect("https://evil.example/"))
h.check("nor any other scheme", not is_safe_redirect("javascript:x"))
h.check("nor a relative path", not is_safe_redirect("x/y"))
h.check("nor an empty target", not is_safe_redirect(""))
h.check("a missing target is not", not is_safe_redirect(nil))
local now = os.time()
local cookie = secure_value("username", "alice", now + 3600)
h.equals("a signed value verifies and comes back", validate_value("username", cookie), "alice")
cookie = secure_value("username", "a|b", now + 3600)
h.equals("a value holding the separator survives the round trip", validate_value("username", cookie), "a|b")
cookie = secure_value("username", "a\nb", now + 3600)
h.equals("a signed control character is still rejected on the way out",
validate_value("username", cookie), nil)
cookie = secure_value("redirect", "/repo/?a=b", 0)
h.equals("an expiration of zero never expires", validate_value("redirect", cookie), "/repo/?a=b")
cookie = secure_value("username", "alice", now - 10)
h.equals("an expired value is rejected", validate_value("username", cookie), nil)
local real_time = os.time
os.time = function()
return now
end
h.equals("a value is rejected at its expiry second",
validate_value("username", secure_value("username", "alice", now)), nil)
h.equals("and accepted the second before",
validate_value("username", secure_value("username", "alice", now + 1)), "alice")
os.time = real_time
cookie = secure_value("username", "alice", now + 3600)
local flipped = cookie:sub(1, -2) .. (cookie:sub(-1) == "0" and "1" or "0")
h.equals("a tampered signature is rejected", validate_value("username", flipped), nil)
h.equals("a value signed for one field does not serve another", validate_value("redirect", cookie), nil)
local function with_field(token, index, replacement)
local fields = {}
for part in token:gmatch("[^|]+") do
fields[#fields + 1] = part
end
fields[index] = replacement
return table.concat(fields, "|")
end
h.equals("a cookie rebuilt unchanged still verifies",
validate_value("username", with_field(cookie, 1, "username")), "alice")
h.equals("a later expiry under the same signature does not",
validate_value("username", with_field(cookie, 3, tostring(now + 7200))), nil)
h.equals("nor does another salt", validate_value("username", with_field(cookie, 4, string.rep("0", 32))), nil)
h.equals("nor another name", validate_value("username", with_field(cookie, 2, "mallory")), nil)
local way_out = secure_value("redirect", "/repo/", 0)
h.equals("nor a way back pointed at another page",
validate_value("redirect", with_field(way_out, 2, url_encode("/other/"))), nil)
h.equals("no cookie does not verify", validate_value("username", nil), nil)
h.equals("a tiny cookie does not verify", validate_value("username", "ab"), nil)
h.equals("a leading separator does not verify", validate_value("username", "|" .. cookie), nil)
h.equals("a trailing field does not verify", validate_value("username", cookie .. "|x"), nil)
h.equals("an unsigned cookie does not verify",
validate_value("username", "username|alice|123|salt"), nil)
h.equals("an exponent spelling of the expiry does not verify, even signed",
validate_value("username", secure_value("username", "alice", "1e10")), nil)
h.equals("an empty value signs to nothing", secure_value("username", "", 1), "")
h.reset()
set_cookie("cgitauth", "value")
local out = h.output()
h.contains("a session cookie carries the hardening attributes", out,
"Set-Cookie: cgitauth=value; HttpOnly; SameSite=Lax; Path=/; Secure; " ..
"Max-Age=604800\n")
h.reset()
set_cookie("cgitauth", "")
h.contains("an empty value clears the cookie instead", h.output(),
"Set-Cookie: cgitauth=; HttpOnly; SameSite=Lax; Path=/; Secure; " ..
"Max-Age=0\n")
h.reset()
redirect_to("/x\r\nX-Evil: 1")
out = h.output()
h.contains("a newline cannot split the location header", out,
"Location: /xX-Evil: 1\n")
h.excludes("no carriage return slips through", out, "\r")
local function run_action(action, opts)
opts = opts or {}
h.reset()
filter_open(action, opts.cookie or "", opts.method or "GET", "", "",
"/", "example.org", opts.https or "on", opts.repo or "", "summary",
opts.url or "/repo/", "/?p=login")
if opts.body then
filter_write(opts.body)
end
local ret = filter_close()
return h.output(), ret
end
local function login(user, pass, redirect)
local body = "username=" .. url_encode(user) .. "&password=" .. url_encode(pass)
if redirect then
body = body .. "&redirect=" .. url_encode(redirect)
end
return run_action("authenticate-post", { method = "POST", body = body })
end
local function visit(repo, token)
local _, ret = run_action("authenticate-cookie",
{ repo = repo, cookie = token and session_name .. "=" .. token })
return ret
end
local function way_back_in(form)
local token = form:match("name='redirect' value='([^']*)'")
return token and validate_value("redirect", token)
end
h.equals("a public repository needs no cookie", visit("unlisted"), 1)
h.equals("a protected repository turns a bare request away", visit("secret-repo"), 0)
local session = secure_value("username", "Alice", now + 3600, stored_hash)
h.equals("a signed session for a member is let through", visit("secret-repo", session), 1)
local stale = secure_value("username", "Alice", now + 3600, "$6$rounds=300000$old$hash")
h.equals("a session from before a password change is turned away", visit("secret-repo", stale), 0)
local orphan = secure_value("username", "bob", now + 3600, stored_hash)
h.equals("a session for a member with no account is turned away", visit("secret-repo", orphan), 0)
local unbound = secure_value("username", "Alice", now + 3600)
h.equals("a session signed without its binding does not verify",
validate_value("username", unbound, account_hash), nil)
h.equals("a memberless repository denies even a valid session", visit("empty-repo", session), 0)
h.equals("a repository of two groups lets in a member of the first", visit("both-repo", session), 1)
if variant == "file" then
h.redirect_file("/etc/cgit-auth/groups", directory .. "/auth-groups-missing")
h.equals("as does one whose groups file is missing", visit("secret-repo", session), 0)
h.redirect_file("/etc/cgit-auth/groups", directory .. "/auth-groups")
end
local outsider = secure_value("username", "carol", now + 3600, stored_hash)
h.equals("a signed session for an outsider is turned away", visit("secret-repo", outsider), 0)
h.equals("while one in the second group lets them in", visit("both-repo", outsider), 1)
local forged = session:sub(1, -2) .. (session:sub(-1) == "0" and "1" or "0")
h.equals("a forged session is turned away", visit("secret-repo", forged), 0)
local real_get_secret = get_secret
get_secret = function()
return string.rep("fedcba9876543210", 4)
end
h.equals("a session signed under another secret does not verify",
validate_value("username", session, account_hash), nil)
h.equals("and lets nobody in", visit("secret-repo", session), 0)
get_secret = real_get_secret
h.equals("while under its own secret it still does", visit("secret-repo", session), 1)
local _, ret = run_action("no-such-action", {})
h.equals("an unknown action denies rather than raising", ret, 0)
out = run_action("body", { url = "/repo/log/?q=x" })
h.contains("the login form posts to the login url", out, "<form class='login' method='post' action='/?p=login'>")
h.contains("and its table is read as layout, not data", out, "<table role='presentation'>")
h.contains("the form names its fields as a login reads them", out, "<input id='username' name='username'")
h.contains("the password among them", out, "<input id='password' name='password'")
h.equals("a first visit says nothing failed", out:find("login-error", 1, true), nil)
h.equals("the form carries a signed way back", way_back_in(out), "/repo/log/?q=x")
out = run_action("body", { cookie = session_name .. "=" .. outsider })
h.contains("a visitor signed in without access is told so", out,
"<p class='login-error' role='alert'>You are signed in but have no access to this repository.</p>")
h.check("and can still sign in as someone else", way_back_in(out) ~= nil)
out = run_action("body", { cookie = session_name .. "=" .. forged })
h.excludes("a forged session is not taken for a signed-in visitor", out, "signed in")
out = run_action("body", { https = "" })
h.contains("over plain http the form says logging in needs https", out,
"<p class='login-error' role='alert'>Logging in only works over https.</p>")
h.check("and is still there to fill in", way_back_in(out) ~= nil)
out = run_action("body", {})
h.excludes("over https it says nothing of the kind", out, "only works over https")
out = run_action("body", { url = "//evil.example/x" })
h.equals("an unsafe destination is swapped for the login page", way_back_in(out), "/?p=login")
h.equals("and is not taken for a failed login", out:find("login-error", 1, true), nil)
out = run_action("body", { url = "https://evil.example/x" })
h.equals("and so is one with a scheme", way_back_in(out), "/?p=login")
out = run_action("body", { url = "/repo/log/?q=x&login=failed" })
h.contains("a failed login says so", out,
"<p class='login-error' role='alert'>Wrong username or password.</p>")
h.equals("and leaves the flag out of the way back", way_back_in(out), "/repo/log/?q=x")
load_script(nil, directory .. "/no-such-directory/auth-secret")
out = run_action("body", {})
h.contains("a secret that cannot be made leaves a page saying so", out,
"<p class='login-error' role='alert'>Logins are unavailable. The server's error log says why.</p>")
h.excludes("with no form to fill in", out, "<form")
h.contains("and the error log names the directory", h.error_log(),
"as " .. directory .. "/no-such-directory is missing or not writable by cgit\n")
load_script()
local way_back = secure_value("redirect", "/repo/", 0)
local function session_expiry(token)
return tonumber(token and token:match("^[^|]+|[^|]+|(%d+)|"))
end
local before = os.time()
out = login("Alice", password, way_back)
h.contains("a good login redirects back", out, "Status: 302")
h.contains("to where the form said", out, "Location: /repo/\n")
local granted = out:match("Set%-Cookie: __Host%-cgitauth=([^;]*);")
h.equals("and grants a session that verifies", granted and validate_value("username", granted, account_hash), "Alice")
local expires = session_expiry(granted)
h.check("which expires a week after the login",
expires and expires >= before + 604800 and expires <= os.time() + 604800, tostring(expires))
local real_constant_equals = constant_equals
local compared = {}
constant_equals = function(a, b)
compared[#compared + 1] = { a, b }
return real_constant_equals(a, b)
end
local function was_compared(value)
for _, pair in ipairs(compared) do
if pair[1] == value or pair[2] == value then
return true
end
end
return false
end
visit("secret-repo", session)
h.check("a visit compares the signature in constant time", was_compared(session:match("[^|]+$")))
compared = {}
login("Alice", password, way_back)
h.check("and a login the password's hash", was_compared(stored_hash))
constant_equals = real_constant_equals
h.equals("and logs nothing", h.error_log(), "")
out = login("Alice", "wrong", way_back)
h.contains("a bad password redirects the same way", out, "Status: 302")
h.contains("back to the page with the failure flagged", out, "Location: /repo/?login=failed\n")
h.contains("but clears the session cookie", out, "Set-Cookie: " .. session_name .. "=; ")
h.contains("and is logged with the name and the page", h.error_log(),
"[cgit] Failed login as Alice for /repo/\n")
h.excludes("but never with the password", h.error_log(), "wrong")
login("eve\r\n[cgit] Forged", "x", way_back)
h.contains("a name cannot forge a log line of its own", h.error_log(),
"[cgit] Failed login as eve[cgit] Forged for /repo/\n")
crypt_settings = {}
out = login("nobody", "x", way_back)
h.contains("an unknown user is told nothing different", out, "Set-Cookie: " .. session_name .. "=; ")
h.contains("and lands on the same flagged page", out, "Location: /repo/?login=failed\n")
h.equals("a missing account spends crypt on a configured hash", crypt_settings[1], stored_hash)
if variant == "file" then
h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users-missing")
crypt_settings = {}
login("nobody", "x", way_back)
h.equals("with no accounts at all nothing is spent", #crypt_settings, 0)
write_file(directory .. "/auth-users-unparsable", "not a parsable line\nnor this one\n")
h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users-unparsable")
crypt_settings = {}
out = login("Alice", password, way_back)
h.contains("a users file with no line that parses turns a login down", out,
"Location: /repo/?login=failed\n")
h.equals("and spends nothing either", #crypt_settings, 0)
h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users")
end
crypt_settings = {}
out = login("Alice", string.rep("a", 257), way_back)
h.contains("an overlong password is turned down", out, "Set-Cookie: " .. session_name .. "=; ")
h.equals("without spending crypt on it", #crypt_settings, 0)
login("Alice", string.rep("a", 256), way_back)
h.equals("a password at the cap is still checked", #crypt_settings, 1)
out = login("Alice", password)
h.contains("a post without the signed token is not served", out, "Status: 404")
out = login("Alice", password, secure_value("redirect", "//evil.example/", 0))
h.contains("even a signed unsafe destination is not followed", out, "Status: 404")
out = login("Alice", password, secure_value("redirect", "https://evil.example/", 0))
h.contains("nor a signed one with a scheme", out, "Status: 404")
load_script({ { "local cookie_insecure = false", "local cookie_insecure = true" } })
out = login("Alice", password, way_back)
h.contains("an insecure session cookie keeps its plain name", out, "Set-Cookie: cgitauth=")
h.excludes("and is not marked Secure", out, "; Secure")
out = run_action("body", { https = "" })
h.excludes("so its form does not ask for https", out, "only works over https")
load_script()
load_script({ { "local session_seconds = 7 * 24 * 60 * 60",
"local session_seconds = 1.5 * 24 * 60 * 60" } })
before = os.time()
out = login("Alice", password, way_back)
granted = out:match("Set%-Cookie: __Host%-cgitauth=([^;]*);")
h.equals("a session a day and a half long still verifies",
granted and validate_value("username", granted, account_hash), "Alice")
expires = session_expiry(granted)
h.check("and expires as long after the login",
expires and expires >= before + 129600 and expires <= os.time() + 129600, tostring(expires))
h.contains("and its cookie lasts as many whole seconds", out, "; Max-Age=129600\n")
load_script()
local libraries = { "openssl.rand", "openssl.hmac", "posix.sys.stat", "posix.unistd" }
local function without(name)
package.loaded[name] = nil
h.preload_failure(name)
end
for _, name in ipairs(libraries) do
local loaded, loader = package.loaded[name], package.preload[name]
without(name)
load_script(nil, directory .. "/unmade-secret")
out = run_action("body", {})
h.contains("without " .. name .. " the login page says logins are unavailable", out,
"Logins are unavailable.")
h.excludes("without " .. name .. " it has no form", out, "<form")
h.contains("without " .. name .. " the error log names it", h.error_log(), name)
local visited, visit_err = pcall(visit, "secret-repo", session)
h.check("without " .. name .. " a session fails closed", not visited)
h.contains("without " .. name .. " its error names it", tostring(visit_err), name)
local posted, post_err = pcall(login, "Alice", password, way_back)
h.check("without " .. name .. " a login fails closed", not posted)
h.contains("without " .. name .. " the login's error names it", tostring(post_err), name)
package.loaded[name], package.preload[name] = loaded, loader
end
load_script()
local loaded, loader = package.loaded["posix.unistd"], package.preload["posix.unistd"]
without("posix.unistd")
local posted, post_err = pcall(login, "Alice", password, way_back)
h.check("without posix.unistd a login with a made secret fails closed", not posted)
h.contains("naming posix.unistd", tostring(post_err), "posix.unistd")
package.loaded["posix.unistd"], package.preload["posix.unistd"] = loaded, loader
for _, name in ipairs(libraries) do
without(name)
end
load_script()
h.equals("without the libraries a public repository is served", visit("unlisted"), 1)
local visited, visit_err = pcall(visit, "secret-repo", session)
h.check("while a protected one fails closed", not visited)
h.contains("with an error naming the library it needed", tostring(visit_err), "openssl.hmac")
out = run_action("body", {})
h.contains("and its login page says logins are unavailable", out, "Logins are unavailable.")
h.excludes("and no form to fill in", out, "<form")
h.contains("while the error log names the missing library", h.error_log(), "openssl.rand")
h.finish()
|