test(docker,flows): cover the listing cap paths, bound the container-list path count

The truncation slice and the demux stdout byte-cap had no docker-layer tests —
only the handler's Truncated wiring was exercised through the fake, so a
mis-slice or a dropped cap would have gone unnoticed. Extract find-output
parsing into a pure parseFindEntries and take the byte cap as a demuxExecStdout
parameter, then unit-test both boundaries (at cap / cap+1 / over-limit stream).

Also bound how many paths one container-files request may list
(maxContainerListPaths), so the per-path entry cap can't be multiplied by an
attacker-chosen path count into a large fan-out or response body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-12 23:22:33 +07:00
co-authored by Claude Opus 4.8
parent 18234f05a3
commit 7cd22ccbc9
4 changed files with 119 additions and 21 deletions
+13
View File
@@ -38,6 +38,11 @@ import (
// resources/ ← user resources copied from user_resources table; also pushed to container /work/resources/
// container/ ← files synced from the container via pull; never sent back to container
// maxContainerListPaths bounds how many directory paths one container-files
// request may list, so the per-path entry cap can't be multiplied into an
// unbounded fan-out or response.
const maxContainerListPaths = 128
type pendingUpload struct {
fileName string
dstPath string
@@ -940,6 +945,14 @@ func (s *FlowFileService) GetFlowContainerFiles(c *gin.Context) {
response.Error(c, response.ErrFlowFilesInvalidRequest, err)
return
}
// Bound the number of paths per request so the per-path entry cap can't be
// multiplied by an attacker-chosen path count into a huge fan-out / response.
if len(containerPaths) > maxContainerListPaths {
err = fmt.Errorf("too many paths requested (%d, limit %d)", len(containerPaths), maxContainerListPaths)
logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("too many container paths")
response.Error(c, response.ErrFlowFilesInvalidRequest, err)
return
}
if len(containerPaths) == 0 {
containerPaths = []string{docker.WorkFolderPathInContainer}
}
@@ -4,6 +4,8 @@ import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
"testing"
"pentagi/pkg/docker"
@@ -132,3 +134,29 @@ func TestGetFlowContainerFiles_TruncatedFlagSurfaced(t *testing.T) {
require.Equal(t, http.StatusOK, *code)
assert.True(t, resp.Truncated)
}
// The per-request path count is bounded so the per-path entry cap can't be
// multiplied by an attacker-chosen number of paths.
func TestGetFlowContainerFiles_TooManyPathsRejected(t *testing.T) {
buildQuery := func(n int) string {
parts := make([]string, n)
for i := range parts {
parts[i] = "paths[]=/p" + strconv.Itoa(i)
}
return strings.Join(parts, "&")
}
// One over the cap → 400 before any docker call.
db := setupFlowFileServiceTestDB(t)
seedFlow(t, db, 1, 1)
svc := NewFlowFileService(db, t.TempDir(), &fakeDockerClient{running: true}, nil)
c, w := newFlowFileTestContext(http.MethodGet,
"/flows/1/files/container?"+buildQuery(maxContainerListPaths+1), nil,
[]string{"flow_files.view", "containers.view"}, 1, 1)
svc.GetFlowContainerFiles(c)
require.Equal(t, http.StatusBadRequest, w.Code)
// Exactly at the cap is allowed.
code, _ := listContainerFiles(t, &fakeDockerClient{running: true}, buildQuery(maxContainerListPaths))
require.Equal(t, http.StatusOK, *code)
}