Skip to content

Commit 4dbddde

Browse files
author
Datanoise
committed
security: always whitelist localhost
1 parent 9f2f398 commit 4dbddde

File tree

6 files changed

+1809
-568
lines changed

6 files changed

+1809
-568
lines changed

config/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package config
22

33
import (
44
"encoding/json"
5-
"golang.org/x/crypto/bcrypt"
65
"os"
6+
7+
"golang.org/x/crypto/bcrypt"
78
)
89

910
const (
@@ -75,6 +76,7 @@ type Config struct {
7576
Transcoders []*TranscoderConfig `json:"transcoders"`
7677
Webhooks []*WebhookConfig `json:"webhooks"`
7778
BannedIPs []string `json:"banned_ips"`
79+
WhitelistedIPs []string `json:"whitelisted_ips"`
7880

7981
AdminPassword string `json:"admin_password"`
8082
AdminUser string `json:"admin_user"`
@@ -219,6 +221,9 @@ func (config *Config) initMapsAndArrays() {
219221
if config.BannedIPs == nil {
220222
config.BannedIPs = make([]string, 0)
221223
}
224+
if config.WhitelistedIPs == nil {
225+
config.WhitelistedIPs = make([]string, 0)
226+
}
222227
if config.AutoDJs == nil {
223228
config.AutoDJs = make([]*AutoDJConfig, 0)
224229
}

server/auth.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,16 @@ func (s *Server) checkAuthLimit(ip string) error {
6262
attempt.Count = 0
6363
}
6464

65+
if s.isWhitelisted(ip) {
66+
return nil
67+
}
6568
return nil
6669
}
6770

6871
func (s *Server) recordAuthFailure(ip string) {
72+
if s.isWhitelisted(ip) {
73+
return
74+
}
6975
s.authAttemptsMu.Lock()
7076
defer s.authAttemptsMu.Unlock()
7177

@@ -95,6 +101,9 @@ func (s *Server) recordAuthSuccess(ip string) {
95101
}
96102

97103
func (s *Server) recordScanAttempt(ip, path string) {
104+
if s.isWhitelisted(ip) {
105+
return
106+
}
98107
s.scanAttemptsMu.Lock()
99108
defer s.scanAttemptsMu.Unlock()
100109

@@ -104,10 +113,8 @@ func (s *Server) recordScanAttempt(ip, path string) {
104113
s.scanAttempts[ip] = attempt
105114
}
106115

107-
if !attempt.Paths[path] {
108-
attempt.Paths[path] = true
109-
attempt.Count++
110-
}
116+
attempt.Paths[path] = true
117+
attempt.Count++
111118

112119
if attempt.Count >= 10 {
113120
attempt.LockoutBy = time.Now().Add(15 * time.Minute)
@@ -167,6 +174,35 @@ func (s *Server) hasAccess(user *config.User, mount string) bool {
167174
return exists
168175
}
169176

177+
func (s *Server) isWhitelisted(ipStr string) bool {
178+
host, _, err := net.SplitHostPort(ipStr)
179+
if err != nil {
180+
host = ipStr
181+
}
182+
183+
ip := net.ParseIP(host)
184+
if ip == nil {
185+
return false
186+
}
187+
188+
if ip.IsLoopback() {
189+
return true
190+
}
191+
192+
for _, whitelisted := range s.Config.WhitelistedIPs {
193+
if strings.Contains(whitelisted, "/") {
194+
_, ipnet, err := net.ParseCIDR(whitelisted)
195+
if err == nil && ipnet.Contains(ip) {
196+
return true
197+
}
198+
}
199+
if whitelisted == host {
200+
return true
201+
}
202+
}
203+
return false
204+
}
205+
170206
func (s *Server) isCSRFSafe(r *http.Request) bool {
171207
if r.Method != http.MethodPost {
172208
return true
@@ -200,6 +236,9 @@ func (s *Server) isCSRFSafe(r *http.Request) bool {
200236
}
201237

202238
func (s *Server) isBanned(ipStr string) bool {
239+
if s.isWhitelisted(ipStr) {
240+
return false
241+
}
203242
host, _, err := net.SplitHostPort(ipStr)
204243
if err != nil {
205244
host = ipStr

server/auth_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package server
2+
3+
import (
4+
"testing"
5+
6+
"github.com/DatanoiseTV/tinyice/config"
7+
)
8+
9+
func TestIPWhitelistAndBanning(t *testing.T) {
10+
cfg := &config.Config{
11+
BannedIPs: []string{"1.2.3.4", "10.0.0.0/24"},
12+
WhitelistedIPs: []string{"1.2.3.4", "192.168.1.1"},
13+
}
14+
s := &Server{
15+
Config: cfg,
16+
authAttempts: make(map[string]*authAttempt),
17+
scanAttempts: make(map[string]*scanAttempt),
18+
}
19+
20+
// 1.2.3.4 is both banned and whitelisted. Whitelist should win.
21+
if s.isBanned("1.2.3.4:1234") {
22+
t.Errorf("1.2.3.4 should NOT be banned as it is whitelisted")
23+
}
24+
25+
// 10.0.0.5 is banned (range) and NOT whitelisted.
26+
if !s.isBanned("10.0.0.5:1234") {
27+
t.Errorf("10.0.0.5 should be banned")
28+
}
29+
30+
// 192.168.1.1 is whitelisted.
31+
if !s.isWhitelisted("192.168.1.1:1234") {
32+
t.Errorf("192.168.1.1 should be whitelisted")
33+
}
34+
35+
// 127.0.0.1 should be always whitelisted
36+
if !s.isWhitelisted("127.0.0.1:1234") {
37+
t.Errorf("127.0.0.1 should be always whitelisted")
38+
}
39+
if !s.isWhitelisted("[::1]:1234") {
40+
t.Errorf("::1 should be always whitelisted")
41+
}
42+
43+
// Verify scan attempt lockout behavior
44+
ip := "8.8.8.8"
45+
path := "/wp-admin"
46+
47+
// Record 9 attempts on SAME path
48+
for i := 0; i < 9; i++ {
49+
s.recordScanAttempt(ip, path)
50+
}
51+
52+
if s.isBanned(ip) {
53+
t.Errorf("IP %s should not be banned yet after 9 attempts", ip)
54+
}
55+
56+
// 10th attempt should ban it
57+
s.recordScanAttempt(ip, path)
58+
if !s.isBanned(ip) {
59+
t.Errorf("IP %s should be banned after 10 attempts on SAME path (due to fix)", ip)
60+
}
61+
62+
// Verify whitelisted IP is NOT banned even after many attempts
63+
wip := "192.168.1.1"
64+
for i := 0; i < 20; i++ {
65+
s.recordScanAttempt(wip, path)
66+
}
67+
if s.isBanned(wip) {
68+
t.Errorf("Whitelisted IP %s should NOT be banned even after many attempts", wip)
69+
}
70+
}

server/handlers_admin.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,39 @@ func (s *Server) handleRemoveBannedIP(w http.ResponseWriter, r *http.Request) {
334334
http.Redirect(w, r, "/admin", http.StatusSeeOther)
335335
}
336336

337+
func (s *Server) handleAddWhitelistedIP(w http.ResponseWriter, r *http.Request) {
338+
if !s.isCSRFSafe(r) {
339+
return
340+
}
341+
user, ok := s.checkAuth(r)
342+
if ok && user.Role == config.RoleSuperAdmin {
343+
ip := r.FormValue("ip")
344+
if ip != "" {
345+
s.Config.WhitelistedIPs = append(s.Config.WhitelistedIPs, ip)
346+
s.Config.SaveConfig()
347+
}
348+
}
349+
http.Redirect(w, r, "/admin", http.StatusSeeOther)
350+
}
351+
352+
func (s *Server) handleRemoveWhitelistedIP(w http.ResponseWriter, r *http.Request) {
353+
if !s.isCSRFSafe(r) {
354+
return
355+
}
356+
user, ok := s.checkAuth(r)
357+
if ok && user.Role == config.RoleSuperAdmin {
358+
ip := r.FormValue("ip")
359+
for i, b := range s.Config.WhitelistedIPs {
360+
if b == ip {
361+
s.Config.WhitelistedIPs = append(s.Config.WhitelistedIPs[:i], s.Config.WhitelistedIPs[i+1:]...)
362+
s.Config.SaveConfig()
363+
break
364+
}
365+
}
366+
}
367+
http.Redirect(w, r, "/admin", http.StatusSeeOther)
368+
}
369+
337370
func (s *Server) handleClearAuthLockout(w http.ResponseWriter, r *http.Request) {
338371
if !s.isCSRFSafe(r) {
339372
http.Error(w, "Forbidden", http.StatusForbidden)

server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ func (s *Server) setupRoutes() *http.ServeMux {
131131
mux.HandleFunc("/admin/remove-user", s.handleRemoveUser)
132132
mux.HandleFunc("/admin/add-banned-ip", s.handleAddBannedIP)
133133
mux.HandleFunc("/admin/remove-banned-ip", s.handleRemoveBannedIP)
134+
mux.HandleFunc("/admin/add-whitelisted-ip", s.handleAddWhitelistedIP)
135+
mux.HandleFunc("/admin/remove-whitelisted-ip", s.handleRemoveWhitelistedIP)
134136
mux.HandleFunc("/admin/clear-auth-lockout", s.handleClearAuthLockout)
135137
mux.HandleFunc("/admin/clear-scan-lockout", s.handleClearScanLockout)
136138
mux.HandleFunc("/admin/add-webhook", s.handleAddWebhook)

0 commit comments

Comments
 (0)