Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/handler/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (h *ComposerHandler) handlePackageMetadata(w http.ResponseWriter, r *http.R
return
}

body, err := io.ReadAll(resp.Body)
body, err := ReadMetadata(resp.Body)
if err != nil {
http.Error(w, "failed to read response", http.StatusInternalServerError)
return
Expand Down
11 changes: 11 additions & 0 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ func containsPathTraversal(path string) bool {
return false
}

// maxMetadataSize is the maximum size of upstream metadata responses (50 MB).
// Package metadata (e.g. npm with many versions) can be large, but unbounded
// reads risk OOM if an upstream misbehaves.
const maxMetadataSize = 50 << 20

// ReadMetadata reads an upstream response body with a size limit to prevent OOM
// from unexpectedly large responses.
func ReadMetadata(r io.Reader) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxMetadataSize))
}

// Proxy provides shared functionality for protocol handlers.
type Proxy struct {
DB *database.DB
Expand Down
3 changes: 1 addition & 2 deletions internal/handler/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
Expand Down Expand Up @@ -93,7 +92,7 @@ func (h *NPMHandler) handlePackageMetadata(w http.ResponseWriter, r *http.Reques
}

// Parse and rewrite tarball URLs
body, err := io.ReadAll(resp.Body)
body, err := ReadMetadata(resp.Body)
if err != nil {
JSONError(w, http.StatusInternalServerError, "failed to read response")
return
Expand Down
2 changes: 1 addition & 1 deletion internal/handler/nuget.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (h *NuGetHandler) handleServiceIndex(w http.ResponseWriter, r *http.Request
return
}

body, err := io.ReadAll(resp.Body)
body, err := ReadMetadata(resp.Body)
if err != nil {
http.Error(w, "failed to read response", http.StatusInternalServerError)
return
Expand Down
2 changes: 1 addition & 1 deletion internal/handler/pub.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (h *PubHandler) handlePackageMetadata(w http.ResponseWriter, r *http.Reques
return
}

body, err := io.ReadAll(resp.Body)
body, err := ReadMetadata(resp.Body)
if err != nil {
http.Error(w, "failed to read response", http.StatusInternalServerError)
return
Expand Down
4 changes: 2 additions & 2 deletions internal/handler/pypi.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func (h *PyPIHandler) handleSimplePackage(w http.ResponseWriter, r *http.Request
return
}

body, err := io.ReadAll(resp.Body)
body, err := ReadMetadata(resp.Body)
if err != nil {
http.Error(w, "failed to read response", http.StatusInternalServerError)
return
Expand Down Expand Up @@ -259,7 +259,7 @@ func (h *PyPIHandler) proxyAndRewriteJSON(w http.ResponseWriter, r *http.Request
return
}

body, err := io.ReadAll(resp.Body)
body, err := ReadMetadata(resp.Body)
if err != nil {
http.Error(w, "failed to read response", http.StatusInternalServerError)
return
Expand Down
34 changes: 34 additions & 0 deletions internal/handler/read_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package handler

import (
"bytes"
"testing"
)

func TestReadMetadata(t *testing.T) {
t.Run("small body", func(t *testing.T) {
data := []byte("hello world")
got, err := ReadMetadata(bytes.NewReader(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !bytes.Equal(got, data) {
t.Errorf("got %q, want %q", got, data)
}
})

t.Run("truncates at limit", func(t *testing.T) {
// Create a reader slightly larger than maxMetadataSize
data := make([]byte, maxMetadataSize+100)
for i := range data {
data[i] = 'x'
}
got, err := ReadMetadata(bytes.NewReader(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != int(maxMetadataSize) {
t.Errorf("got length %d, want %d", len(got), maxMetadataSize)
}
})
}
2 changes: 2 additions & 0 deletions internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ func (h *APIHandler) HandleGetVulns(w http.ResponseWriter, r *http.Request) {
// @Failure 500 {string} string
// @Router /api/outdated [post]
func (h *APIHandler) HandleOutdated(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
var req OutdatedRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
Expand Down Expand Up @@ -372,6 +373,7 @@ func (h *APIHandler) HandleOutdated(w http.ResponseWriter, r *http.Request) {
// @Failure 500 {string} string
// @Router /api/bulk [post]
func (h *APIHandler) HandleBulkLookup(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
var req BulkRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
Expand Down
19 changes: 19 additions & 0 deletions internal/server/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ func TestHandleOutdated_EmptyBody(t *testing.T) {
}
}

func TestHandleOutdated_OversizedBody(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
h := NewAPIHandler(svc, nil)

// Send a body larger than 1 MB
body := make([]byte, 2<<20)
for i := range body {
body[i] = 'x'
}
req := httptest.NewRequest("POST", "/api/outdated", bytes.NewReader(body))
w := httptest.NewRecorder()
h.HandleOutdated(w, req)

if w.Code != http.StatusBadRequest {
t.Errorf("expected status %d for oversized body, got %d", http.StatusBadRequest, w.Code)
}
}

func TestHandleOutdated_InvalidJSON(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
Expand Down