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
10 changes: 10 additions & 0 deletions internal/handler/gem.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ func (h *GemHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
// starts at the first "-<digit>" that leaves such a version and platform,
// so a name with a "-<digit>" segment of its own, such as devise-2fa, is
// kept whole. A filename no split fits is split at the first "-<digit>".
//
// Trying every "-<digit>" costs time quadratic in the filename's length, so
// a filename longer than maxGemFilenameLength is refused.
func (h *GemHandler) parseGemFilename(filename string) (name, version string) {
if len(filename) > maxGemFilenameLength {
return "", ""
}
base := strings.TrimSuffix(filename, ".gem")
first := -1
for i := 0; i+1 < len(base); i++ {
Expand All @@ -119,6 +125,10 @@ func (h *GemHandler) parseGemFilename(filename string) (name, version string) {
return base[:first], base[first+1:]
}

// maxGemFilenameLength bounds the gem filenames parseGemFilename accepts.
// Real gem filenames are a small fraction of this.
const maxGemFilenameLength = 512

var (
gemVersionPattern = regexp.MustCompile(`^[0-9]+(\.[0-9A-Za-z]+)*$`)
gemPlatformPattern = regexp.MustCompile(`^[A-Za-z][0-9A-Za-z_]*(-[0-9A-Za-z_]+)*$`)
Expand Down
25 changes: 25 additions & 0 deletions internal/handler/gem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ func TestGemParseFilename(t *testing.T) {
}
}

func TestGemDownloadRefusesLongFilename(t *testing.T) {
// Every "-1-" could start a version, and each try reads to the "!" at
// the end before failing, so parsing this used to take minutes.
filename := strings.Repeat("-1-aaaaaa", 100_000) + "!.gem"

h := &GemHandler{proxy: testProxy(), upstreamURL: "http://upstream.invalid"}
req := httptest.NewRequest(http.MethodGet, "/gems/x", nil)
req.SetPathValue("filename", "x"+filename)
w := httptest.NewRecorder()

done := make(chan struct{})
go func() {
h.handleDownload(w, req)
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("parsing a long gem filename did not finish within 5s")
}
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}

func TestGemCompactIndexCooldown(t *testing.T) {
now := time.Now()
oldTime := now.Add(-7 * 24 * time.Hour).Format(time.RFC3339)
Expand Down
Loading