Skip to content

Commit 7cf911a

Browse files
committed
Fix Issue #172 and Security Advisory GHSA-rmxw-pq4x-3fvh
1 parent dbe91ee commit 7cf911a

3 files changed

Lines changed: 88 additions & 2 deletions

File tree

assets/js/src/collab.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ function fmtHeaders(headers) {
293293
// ══ HTTP LOG ══
294294
function onHTTP(e) {
295295
ST.httpEvents.unshift(e);
296+
if (ST.httpEvents.length > 1000) ST.httpEvents.length = 1000;
296297
ST.httpCnt++;
297298
updateBadge("http-badge", ST.httpCnt);
298299
updateCollabBadge();
@@ -483,6 +484,7 @@ export function clearHTTP() {
483484
// ══ DNS LOG ══
484485
function onDNS(e) {
485486
ST.dnsEvents.unshift(e);
487+
if (ST.dnsEvents.length > 1000) ST.dnsEvents.length = 1000;
486488
ST.dnsCnt.total++;
487489
if (e.qtype === "A") ST.dnsCnt.A++;
488490
else if (e.qtype === "MX") ST.dnsCnt.MX++;
@@ -560,6 +562,7 @@ export function clearDNS() {
560562
function onSMB(e) {
561563
console.log(e);
562564
ST.smbEvents.unshift(e);
565+
if (ST.smbEvents.length > 1000) ST.smbEvents.length = 1000;
563566
updateBadge("smb-badge", ST.smbEvents.length);
564567
updateCollabBadge();
565568
renderSMB();
@@ -693,6 +696,7 @@ export function clearSMB() {
693696
// ══ LDAP Log ══
694697
function onLDAP(e) {
695698
ST.ldapEvents.unshift(e);
699+
if (ST.ldapEvents.length > 1000) ST.ldapEvents.length = 1000;
696700
updateBadge("ldap-badge", ST.ldapEvents.length);
697701
updateCollabBadge();
698702
renderLDAP();
@@ -871,6 +875,7 @@ export function clearLDAP() {
871875
// ══ SMTP ══
872876
function onSMTP(e) {
873877
ST.smtpEvents.unshift(e);
878+
if (ST.smtpEvents.length > 1000) ST.smtpEvents.length = 1000;
874879
updateBadge("smtp-badge", ST.smtpEvents.length);
875880
updateCollabBadge();
876881
renderSMTP();
@@ -1116,7 +1121,7 @@ export function renderSMTP() {
11161121
empty.style.display = vis.length ? "none" : "flex";
11171122
inbox.querySelectorAll(".mail-card").forEach((c) => c.remove());
11181123

1119-
vis.forEach((e, i) => {
1124+
vis.slice(0, 500).forEach((e, i) => {
11201125
inbox.appendChild(buildMailCard(e, i === 0 && !filter));
11211126
});
11221127
}

httpserver/mux_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,74 @@ func TestBulkDownload_Success(t *testing.T) {
578578
require.Equal(t, "application/zip", w.Header().Get("Content-Type"))
579579
}
580580

581+
func TestBulkDownload_ACL_NoCredentials(t *testing.T) {
582+
dir := t.TempDir()
583+
protected := filepath.Join(dir, "protected")
584+
require.NoError(t, os.Mkdir(protected, 0755))
585+
require.NoError(t, os.WriteFile(filepath.Join(protected, "secret.txt"), []byte("secret"), 0644))
586+
587+
hash, err := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.MinCost)
588+
require.NoError(t, err)
589+
acl := fmt.Sprintf(`{"auth":"user:%s"}`, hash)
590+
require.NoError(t, os.WriteFile(filepath.Join(protected, ".goshs"), []byte(acl), 0644))
591+
592+
fs, _ := newTestFileServer(t, dir)
593+
r := httptest.NewRequest(http.MethodGet, "/?bulk&file=/protected/secret.txt", nil)
594+
w := httptest.NewRecorder()
595+
fs.bulkDownload(w, r)
596+
require.Equal(t, http.StatusUnauthorized, w.Code)
597+
}
598+
599+
func TestBulkDownload_ACL_WithCredentials(t *testing.T) {
600+
dir := t.TempDir()
601+
protected := filepath.Join(dir, "protected")
602+
require.NoError(t, os.Mkdir(protected, 0755))
603+
require.NoError(t, os.WriteFile(filepath.Join(protected, "secret.txt"), []byte("secret"), 0644))
604+
605+
hash, err := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.MinCost)
606+
require.NoError(t, err)
607+
acl := fmt.Sprintf(`{"auth":"user:%s"}`, hash)
608+
require.NoError(t, os.WriteFile(filepath.Join(protected, ".goshs"), []byte(acl), 0644))
609+
610+
fs, _ := newTestFileServer(t, dir)
611+
r := httptest.NewRequest(http.MethodGet, "/?bulk&file=/protected/secret.txt", nil)
612+
r.Header.Set("Authorization", basicAuthHeader("user", "pass"))
613+
w := httptest.NewRecorder()
614+
fs.bulkDownload(w, r)
615+
require.Equal(t, http.StatusOK, w.Code)
616+
require.Equal(t, "application/zip", w.Header().Get("Content-Type"))
617+
}
618+
619+
func TestBulkDownload_ACL_BlockList(t *testing.T) {
620+
dir := t.TempDir()
621+
require.NoError(t, os.WriteFile(filepath.Join(dir, "blocked.txt"), []byte("nope"), 0644))
622+
require.NoError(t, os.WriteFile(filepath.Join(dir, ".goshs"), []byte(`{"block":["blocked.txt"]}`), 0644))
623+
624+
fs, _ := newTestFileServer(t, dir)
625+
r := httptest.NewRequest(http.MethodGet, "/?bulk&file=/blocked.txt", nil)
626+
w := httptest.NewRecorder()
627+
fs.bulkDownload(w, r)
628+
require.Equal(t, http.StatusNotFound, w.Code)
629+
}
630+
631+
func TestBulkDownload_ACL_InheritedFromParent(t *testing.T) {
632+
dir := t.TempDir()
633+
sub := filepath.Join(dir, "sub")
634+
require.NoError(t, os.Mkdir(sub, 0755))
635+
require.NoError(t, os.WriteFile(filepath.Join(sub, "file.txt"), []byte("data"), 0644))
636+
637+
hash, err := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.MinCost)
638+
require.NoError(t, err)
639+
acl := fmt.Sprintf(`{"auth":"user:%s"}`, hash)
640+
require.NoError(t, os.WriteFile(filepath.Join(dir, ".goshs"), []byte(acl), 0644))
641+
642+
fs, _ := newTestFileServer(t, dir)
643+
r := httptest.NewRequest(http.MethodGet, "/?bulk&file=/sub/file.txt", nil)
644+
w := httptest.NewRecorder()
645+
fs.bulkDownload(w, r)
646+
require.Equal(t, http.StatusUnauthorized, w.Code)
647+
}
648+
581649
// ─── returnJsonDirListing tests ──────────────────────────────────────────────
582650

583651
func TestReturnJsonDirListing(t *testing.T) {

httpserver/updown.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"os"
1010
"path/filepath"
11+
"slices"
1112
"strings"
1213
"time"
1314

@@ -229,12 +230,24 @@ func (fs *FileServer) bulkDownload(w http.ResponseWriter, req *http.Request) {
229230
return
230231
}
231232

232-
// Validate each path and collect absolute paths; skip any traversal attempts
233+
// Validate each path, enforce .goshs ACL, and collect absolute paths.
234+
// ACL checks must happen here, before any response headers are written,
235+
// so that applyCustomAuth/handleError can still send a proper HTTP error.
233236
for _, file := range files {
234237
absPath, err := sanitizePath(fs.Webroot, file)
235238
if err != nil {
236239
continue
237240
}
241+
acl, aclErr := fs.findEffectiveACL(filepath.Dir(absPath))
242+
if aclErr == nil {
243+
if ok := fs.applyCustomAuth(w, req, acl); !ok {
244+
return
245+
}
246+
if slices.Contains(acl.Block, filepath.Base(absPath)) {
247+
fs.handleError(w, req, fmt.Errorf("requested file is blocked"), http.StatusNotFound)
248+
return
249+
}
250+
}
238251
filesCleaned = append(filesCleaned, absPath)
239252
}
240253

0 commit comments

Comments
 (0)