gitlyx / security_test.v
98 lines · 85 sloc · 3.93 KB · 13ca859655a005b2a87a4935764694302b4f2fda
Raw
1module main
2
3import crypto.sha256
4
5// Regression tests for the input validation that guards git command
6// construction in create_file_in_bare_repo (see repo/file_routes.v). These
7// inputs used to be interpolated into shell strings; they are now passed to
8// git as plain arguments, but we still reject values git could misread as
9// flags/refs or that contain control characters.
10
11fn test_is_valid_repo_file_path_accepts_normal_paths() {
12 assert is_valid_repo_file_path('README.md')
13 assert is_valid_repo_file_path('src/main.v')
14 assert is_valid_repo_file_path('dir/file,with,commas.txt')
15 assert is_valid_repo_file_path('a/b/c/d.e')
16}
17
18fn test_is_valid_repo_file_path_rejects_dangerous_paths() {
19 assert !is_valid_repo_file_path('')
20 assert !is_valid_repo_file_path('/etc/passwd') // absolute
21 assert !is_valid_repo_file_path('-rf') // looks like a flag
22 assert !is_valid_repo_file_path('../../etc/passwd') // traversal
23 assert !is_valid_repo_file_path('a/../../b') // traversal
24 assert !is_valid_repo_file_path('file\nname') // newline
25 assert !is_valid_repo_file_path('file\x00name') // NUL
26 assert !is_valid_repo_file_path('a\tb') // tab / control char
27}
28
29fn test_is_safe_ref_accepts_normal_branches() {
30 assert is_safe_ref('master')
31 assert is_safe_ref('feature/new-thing')
32 assert is_safe_ref('release-1.2.3')
33}
34
35fn test_is_safe_ref_rejects_injection_attempts() {
36 assert !is_safe_ref('')
37 assert !is_safe_ref('--upload-pack=touch /tmp/pwned') // leading dash + space
38 assert !is_safe_ref('master;rm -rf /') // shell metacharacters
39 assert !is_safe_ref('master$(whoami)')
40 assert !is_safe_ref('master`id`')
41 assert !is_safe_ref('a..b') // ref traversal
42 assert !is_safe_ref('branch with spaces')
43}
44
45// Webhook SSRF guard: the IP classifiers must reject internal destinations and
46// allow public ones. (is_safe_webhook_url itself does DNS and isn't unit-tested.)
47
48fn test_is_blocked_ipv4_blocks_internal_ranges() {
49 assert is_blocked_ipv4('127.0.0.1') // loopback
50 assert is_blocked_ipv4('10.1.2.3') // private
51 assert is_blocked_ipv4('172.16.5.5') // private
52 assert is_blocked_ipv4('172.31.255.255') // private (edge)
53 assert is_blocked_ipv4('192.168.0.1') // private
54 assert is_blocked_ipv4('169.254.169.254') // link-local / cloud metadata
55 assert is_blocked_ipv4('0.0.0.0') // unspecified
56 assert is_blocked_ipv4('100.64.0.1') // CGNAT
57 assert is_blocked_ipv4('224.0.0.1') // multicast
58 assert is_blocked_ipv4('garbage') // unparseable -> fail closed
59}
60
61fn test_is_blocked_ipv4_allows_public() {
62 assert !is_blocked_ipv4('8.8.8.8')
63 assert !is_blocked_ipv4('1.1.1.1')
64 assert !is_blocked_ipv4('172.32.0.1') // just outside 172.16/12
65 assert !is_blocked_ipv4('172.15.0.1') // just outside 172.16/12
66 assert !is_blocked_ipv4('93.184.216.34')
67}
68
69fn test_is_blocked_ipv6() {
70 assert is_blocked_ipv6('::1') // loopback
71 assert is_blocked_ipv6('::') // unspecified
72 assert is_blocked_ipv6('fe80::1') // link-local
73 assert is_blocked_ipv6('fc00::1') // unique-local
74 assert is_blocked_ipv6('fd12:3456::1') // unique-local
75 assert is_blocked_ipv6('::ffff:127.0.0.1') // IPv4-mapped loopback
76 assert !is_blocked_ipv6('2606:4700:4700::1111') // public
77 assert !is_blocked_ipv6('::ffff:8.8.8.8') // IPv4-mapped public
78}
79
80// Password hashing: new hashes must be bcrypt, and legacy salted-SHA-256 hashes
81// must still verify (so existing users aren't locked out before re-login).
82
83fn test_new_passwords_are_bcrypt() {
84 h := hash_password_with_salt('s3cret-pw', 'ignored-salt')
85 assert h.starts_with('$2') // bcrypt hash
86 assert !password_hash_is_legacy(h)
87 assert compare_password_with_hash('s3cret-pw', 'ignored-salt', h)
88 assert !compare_password_with_hash('wrong-pw', 'ignored-salt', h)
89}
90
91fn test_legacy_sha256_hashes_still_verify() {
92 salt := 'abc123'
93 // Legacy scheme was sha256('${password}${salt}').
94 legacy := sha256.sum('hunter2${salt}'.bytes()).hex()
95 assert password_hash_is_legacy(legacy)
96 assert compare_password_with_hash('hunter2', salt, legacy)
97 assert !compare_password_with_hash('nope', salt, legacy)
98}
99