Skip to content

Commit 8843a4d

Browse files
authored
Merge pull request #13 from ybizeul/11-arbitrary-file-read-and-deletion-vulnerability
fix: various path traversal security issues
2 parents bd50b26 + f8b69b5 commit 8843a4d

File tree

5 files changed

+183
-39
lines changed

5 files changed

+183
-39
lines changed

internal/feed/feed.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ func (feed *Feed) GetPublicItem(i string) (*PublicFeedItem, error) {
250250
return nil, FeedErrorInvalidFeedItem
251251
}
252252

253-
s, err := os.Stat(path.Join(feed.Path, i))
253+
s, err := os.Stat(path.Join(feed.Path, path.Join("/", i)))
254254

255255
if err != nil {
256256
if os.IsNotExist(err) {
@@ -276,8 +276,11 @@ func (feed *Feed) GetItemData(item string) ([]byte, error) {
276276
var content []byte
277277

278278
// Get path to feed item
279-
filePath := path.Join(feed.Path, item)
279+
filePath := path.Join(feed.Path, path.Join("/"+item))
280280

281+
if path.Base(filePath) == "secret" || path.Base(filePath) == "pin" || path.Base(filePath) == "config.json" {
282+
return nil, fmt.Errorf("%w: %s", FeedErrorItemNotFound, item)
283+
}
281284
// Read feed item content
282285
content, err := os.ReadFile(filePath)
283286
if err != nil {
@@ -315,7 +318,7 @@ func (feed *Feed) IsSecretValid(secret string) error {
315318

316319
// AddItem reads content from r and creates a new file in the feed directory
317320
// with a name and file extension based on contentType, then notifies clients
318-
func (f *Feed) AddItem(contentType string, r io.ReadCloser) error {
321+
func (f *Feed) AddItem(contentType string, r io.Reader) error {
319322
fL.Logger.Debug("Adding Item", slog.String("feed", f.Name()), slog.String("content-type", contentType))
320323

321324
var err error
@@ -385,10 +388,11 @@ func (f *Feed) AddItem(contentType string, r io.ReadCloser) error {
385388
}
386389

387390
// Notify additon to all connected browsers
388-
if err = f.WebSocketManager.NotifyAdd(publicItem); err != nil {
389-
return err
391+
if f.WebSocketManager != nil {
392+
if err = f.WebSocketManager.NotifyAdd(publicItem); err != nil {
393+
return err
394+
}
390395
}
391-
392396
// Send push notification to subscribed browsers
393397
err = f.sendPushNotification()
394398
if err != nil {
@@ -405,7 +409,7 @@ func (f *Feed) AddItem(contentType string, r io.ReadCloser) error {
405409
func (f *Feed) RemoveItem(item string) error {
406410
fL.Logger.Debug("Remove Item", slog.String("name", item), slog.String("feed", f.Path))
407411

408-
itemPath := path.Join(f.Path, item)
412+
itemPath := path.Join(f.Path, path.Join("/", item))
409413

410414
// Save public item before deletion for notification later
411415
publicItem, err := f.GetPublicItem(item)
@@ -423,8 +427,10 @@ func (f *Feed) RemoveItem(item string) error {
423427
}
424428

425429
// Notify all connected websockets
426-
if err = f.WebSocketManager.NotifyRemove(publicItem); err != nil {
427-
return err
430+
if f.WebSocketManager != nil {
431+
if err = f.WebSocketManager.NotifyRemove(publicItem); err != nil {
432+
return err
433+
}
428434
}
429435

430436
fL.Logger.Debug("Removed Item", slog.String("name", item), slog.String("feed", f.Path))

internal/feed/feed_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package feed
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"testing"
7+
)
8+
9+
func TestGetFeedItemData(t *testing.T) {
10+
t.Cleanup(func() {
11+
os.RemoveAll("tests/feed1")
12+
})
13+
f, err := NewFeed("tests/feed1")
14+
if err != nil {
15+
t.Fatal(err)
16+
}
17+
18+
reader := bytes.NewReader([]byte("test"))
19+
20+
err = f.AddItem("text/plain", reader)
21+
if err != nil {
22+
t.Fatal(err)
23+
}
24+
25+
pf, err := f.Public()
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
i := pf.Items[0]
30+
b, err := f.GetItemData(i.Name)
31+
if len(b) == 0 || err != nil {
32+
t.Fatal(err)
33+
}
34+
}
35+
36+
func TestPathTraversalGet(t *testing.T) {
37+
t.Cleanup(func() {
38+
os.RemoveAll("tests/feed1")
39+
os.RemoveAll("tests/feed2")
40+
})
41+
_, err := NewFeed("tests/feed1")
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
46+
f, err := NewFeed("tests/feed2")
47+
if err != nil {
48+
t.Fatal(err)
49+
}
50+
51+
b, err := f.GetItemData("../feed1/config.json")
52+
53+
if len(b) != 0 || err == nil {
54+
t.Fatal("Path traversal not blocked")
55+
}
56+
}
57+
58+
func TestPathTraversalDelete(t *testing.T) {
59+
t.Cleanup(func() {
60+
os.RemoveAll("tests/feed1")
61+
os.RemoveAll("tests/feed2")
62+
})
63+
_, err := NewFeed("tests/feed1")
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
68+
f, err := NewFeed("tests/feed2")
69+
if err != nil {
70+
t.Fatal(err)
71+
}
72+
73+
err = f.RemoveItem("../feed1/config.json")
74+
75+
if err == nil {
76+
t.Fatal("Path traversal not blocked")
77+
}
78+
}
79+
80+
func TestPathTraversalPublicItem(t *testing.T) {
81+
t.Cleanup(func() {
82+
os.RemoveAll("tests/feed1")
83+
os.RemoveAll("tests/feed2")
84+
})
85+
_, err := NewFeed("tests/feed1")
86+
if err != nil {
87+
t.Fatal(err)
88+
}
89+
90+
f, err := NewFeed("tests/feed2")
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
95+
p, err := f.GetPublicItem("../feed1/config.json")
96+
97+
if p != nil || err == nil {
98+
t.Fatal("Path traversal not blocked")
99+
}
100+
}
101+
102+
func TestPublicItem(t *testing.T) {
103+
t.Cleanup(func() {
104+
os.RemoveAll("tests/feed1")
105+
})
106+
f, err := NewFeed("tests/feed1")
107+
if err != nil {
108+
t.Fatal(err)
109+
}
110+
111+
reader := bytes.NewReader([]byte("test"))
112+
113+
err = f.AddItem("text/plain", reader)
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
118+
pf, err := f.Public()
119+
if err != nil {
120+
t.Fatal(err)
121+
}
122+
i := pf.Items[0]
123+
124+
p, err := f.GetPublicItem(i.Name)
125+
126+
if p == nil || err != nil {
127+
t.Fatal(err)
128+
}
129+
}

internal/feed/websocket.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,15 @@ func (m *WebSocketManager) RunSocketForFeed(feedName string, w http.ResponseWrit
8989
m.FeedSockets = append(m.FeedSockets, feedSockets)
9090
}
9191

92+
// Upgrade http connection to websocket
9293
c, err := upgrader.Upgrade(w, r, nil)
9394
if err != nil {
9495
utils.CloseWithCodeAndMessage(w, 500, "Unable to upgrade WebSocket")
9596
}
9697

9798
feedSockets.websockets = append(feedSockets.websockets, c)
9899

100+
// Get provided secret and validate feed access
99101
secret, _ := utils.GetSecret(r)
100102

101103
f, err := m.FeedManager.GetFeedWithAuth(feedName, secret)
@@ -113,19 +115,26 @@ func (m *WebSocketManager) RunSocketForFeed(feedName string, w http.ResponseWrit
113115
}
114116
}
115117

118+
// Cleanup
116119
defer func() {
117120
feedSockets.RemoveConn(c)
118121
c.Close()
119122
}()
120123

124+
// Start waiting for messages
121125
for {
122126
mt, message, err := c.ReadMessage()
123-
wsL.Logger.Debug("Message Received", slog.String("message", string(message)), slog.Int("messageType", mt))
127+
wsL.Logger.Debug("Message Received",
128+
slog.String("message", string(message)),
129+
slog.Int("messageType", mt))
124130
if err != nil {
125-
slog.Error("Error reading message", slog.String("error", err.Error()), slog.Int("messageType", mt))
131+
slog.Error("Error reading message",
132+
slog.String("error", err.Error()),
133+
slog.Int("messageType", mt))
126134
break
127135
}
128136
switch strings.TrimSpace(string(message)) {
137+
// Return pubic feed content
129138
case "feed":
130139
pf, err := f.Public()
131140
if err != nil {
@@ -139,8 +148,11 @@ func (m *WebSocketManager) RunSocketForFeed(feedName string, w http.ResponseWrit
139148
}
140149
}
141150

151+
// NotifyAdd notifies all connected websockets that an item has been added
142152
func (m *WebSocketManager) NotifyAdd(item *PublicFeedItem) error {
143-
wsL.Logger.Debug("Notify websocket", slog.Any("item", item), slog.Int("ws count", len(m.FeedSockets)))
153+
wsL.Logger.Debug("Notify websocket",
154+
slog.Any("item", item),
155+
slog.Int("ws count", len(m.FeedSockets)))
144156
for _, f := range m.FeedSockets {
145157
wsL.Logger.Debug("checking feed", slog.String("feedName", f.feedName))
146158
if f.feedName == item.Feed.Name {
@@ -158,12 +170,15 @@ func (m *WebSocketManager) NotifyAdd(item *PublicFeedItem) error {
158170
return nil
159171
}
160172

173+
// NotifyRemove notify all connected websockets that an item has been removed
161174
func (m *WebSocketManager) NotifyRemove(item *PublicFeedItem) error {
162-
wsL.Logger.Debug("Notify websocket", slog.Any("item", item), slog.Int("ws count", len(m.FeedSockets)))
175+
wsL.Logger.Debug("Notify websocket",
176+
slog.Any("item", item),
177+
slog.Int("ws count", len(m.FeedSockets)))
163178
for _, f := range m.FeedSockets {
164179
wsL.Logger.Debug("checking feed", slog.String("feedName", f.feedName))
165180
if f.feedName == item.Feed.Name {
166-
wsL.Logger.Debug("Found feed", slog.String("feedName", f.feedName))
181+
wsL.Logger.Debug("found feed", slog.String("feedName", f.feedName))
167182
for _, w := range f.websockets {
168183
if err := w.WriteJSON(FeedNotification{
169184
Action: "remove",

0 commit comments

Comments
 (0)