language: count '_' alongside '-' in ParseAcceptLanguage guard

The BCP 47 scanner aliases '_' to '-' in scanner.init at
internal/language/parse.go lines 104-111, so a payload built
with '_' as the separator bypasses the 1000-dash guard at
language/parse.go:169 (the CVE-2022-32149 fix landed in CL
442235) and re-triggers the O(N^2) gobble path on
attacker-controlled Accept-Language input. Count both
separators in the guard.

Reproduction summary:

  100k tokens with '_' separator:  1.35 s parse time
  Same 100k tokens with '-':       49.9 us (guard fires)
  Amplification: ~27,000x

See golang/go#79684 for the full reproducer with go.mod,
main.go, and measurement output.

Test:

TestParseAcceptLanguageUnderscoreGuard in language/parse_test.go
asserts errTagListTooLarge is returned when 1001 '_' separators
are present.

Filed per the upstream invitation from the Go Security Officers:
the original security@golang.org report was determined to be not
a security issue, with an invitation to file as an upstream
change.

Fixes golang/go#79684

Change-Id: I4e9a582c54a3e7268072870c0b22a3a392cfa1f2
GitHub-Last-Rev: 701c4da23913b82c62e71ade7c80d178f22d7b21
GitHub-Pull-Request: golang/text#66
Reviewed-on: https://go-review.googlesource.com/c/text/+/783500
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
Auto-Submit: Roland Shoemaker <roland@golang.org>
diff --git a/language/parse.go b/language/parse.go
index 053336e..65b53b9 100644
--- a/language/parse.go
+++ b/language/parse.go
@@ -166,7 +166,9 @@
 		}
 	}()
 
-	if strings.Count(s, "-") > 1000 {
+	// The BCP 47 scanner aliases '_' to '-' in scanner.init
+	// (internal/language/parse.go); the guard must count both.
+	if strings.Count(s, "-")+strings.Count(s, "_") > 1000 {
 		return nil, nil, errTagListTooLarge
 	}
 
diff --git a/language/parse_test.go b/language/parse_test.go
index 0eee033..189b24f 100644
--- a/language/parse_test.go
+++ b/language/parse_test.go
@@ -407,3 +407,18 @@
 		t.Errorf("ParseAcceptLanguage() unexpected error: got %v, want %v", err, errTagListTooLarge)
 	}
 }
+
+func TestParseAcceptLanguageUnderscoreGuard(t *testing.T) {
+	// Build a payload that would trigger the quadratic path with
+	// '_' but is blocked by the dash-count guard. Verify the guard
+	// now also counts '_'.
+	parts := make([]string, 0, 1002)
+	parts = append(parts, "en")
+	for i := 0; i < 1001; i++ {
+		parts = append(parts, "abcdefghi")
+	}
+	s := strings.Join(parts, "_")
+	if _, _, err := ParseAcceptLanguage(s); err != errTagListTooLarge {
+		t.Errorf("ParseAcceptLanguage with 1001 '_' separators: got err=%v, want errTagListTooLarge", err)
+	}
+}