Skip to content

Commit

Permalink
Handle multiple ACRH headers (fixes #184)
Browse files Browse the repository at this point in the history
  • Loading branch information
jub0bs committed Aug 29, 2024
1 parent 5f32256 commit 441060e
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 106 deletions.
8 changes: 5 additions & 3 deletions cors.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,11 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) {
// Note: the Fetch standard guarantees that at most one
// Access-Control-Request-Headers header is present in the preflight request;
// see step 5.2 in https://fetch.spec.whatwg.org/#cors-preflight-fetch-0.
reqHeaders, found := first(r.Header, "Access-Control-Request-Headers")
if found && !c.allowedHeadersAll && !c.allowedHeaders.Subsumes(reqHeaders[0]) {
c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders[0])
// However, some gateways split that header into multiple headers of the same name;
// see https://github.com/rs/cors/issues/184.
reqHeaders, found := r.Header["Access-Control-Request-Headers"]
if found && !c.allowedHeadersAll && !c.allowedHeaders.Subsumes(reqHeaders) {
c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders)
return
}
if c.allowedOriginsAll {
Expand Down
40 changes: 40 additions & 0 deletions cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,46 @@ func TestSpec(t *testing.T) {
},
true,
},
{
"MultipleACRHHeaders",
Options{
AllowedOrigins: []string{"http://foobar.com"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
},
"OPTIONS",
http.Header{
"Origin": {"http://foobar.com"},
"Access-Control-Request-Method": {"GET"},
"Access-Control-Request-Headers": {"authorization", "content-type"},
},
http.Header{
"Vary": {"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"},
"Access-Control-Allow-Origin": {"http://foobar.com"},
"Access-Control-Allow-Methods": {"GET"},
"Access-Control-Allow-Headers": {"authorization", "content-type"},
},
true,
},
{
"MultipleACRHHeadersWithOWSAndEmptyElements",
Options{
AllowedOrigins: []string{"http://foobar.com"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
},
"OPTIONS",
http.Header{
"Origin": {"http://foobar.com"},
"Access-Control-Request-Method": {"GET"},
"Access-Control-Request-Headers": {"authorization\t", " ", " content-type"},
},
http.Header{
"Vary": {"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"},
"Access-Control-Allow-Origin": {"http://foobar.com"},
"Access-Control-Allow-Methods": {"GET"},
"Access-Control-Allow-Headers": {"authorization\t", " ", " content-type"},
},
true,
},
}
for i := range cases {
tc := cases[i]
Expand Down
152 changes: 120 additions & 32 deletions internal/sortedset.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,46 +52,134 @@ func (set SortedSet) String() string {
return strings.Join(elems, ",")
}

// Subsumes reports whether csv is a sequence of comma-separated names that are
// - all elements of set,
// - sorted in lexicographically order,
// Subsumes reports whether values is a sequence of list-based field values
// whose elements are
// - all members of set,
// - sorted in lexicographical order,
// - unique.
func (set SortedSet) Subsumes(csv string) bool {
if csv == "" {
return true
func (set SortedSet) Subsumes(values []string) bool {
var ( // effectively constant
maxLen = maxOWSBytes + set.maxLen + maxOWSBytes + 1 // +1 for comma
)
var (
posOfLastNameSeen = -1
name string
commaFound bool
emptyElements int
ok bool
)
for _, s := range values {
for {
// As a defense against maliciously long names in s,
// we process only a small number of s's leading bytes per iteration.
name, s, commaFound = cutAtComma(s, maxLen)
name, ok = trimOWS(name, maxOWSBytes)
if !ok {
return false
}
if name == "" {
// RFC 9110 requires recipients to tolerate
// "a reasonable number of empty list elements"; see
// https://httpwg.org/specs/rfc9110.html#abnf.extension.recipient.
emptyElements++
if emptyElements > maxEmptyElements {
return false
}
if !commaFound { // We have now exhausted the names in s.
break
}
continue
}
pos, ok := set.m[name]
if !ok {
return false
}
// The names in s are expected to be sorted in lexicographical order
// and to each appear at most once.
// Therefore, the positions (in set) of the names that
// appear in s should form a strictly increasing sequence.
// If that's not actually the case, bail out.
if pos <= posOfLastNameSeen {
return false
}
posOfLastNameSeen = pos
if !commaFound { // We have now exhausted the names in s.
break
}
}
}
return true
}

const (
maxOWSBytes = 1 // number of leading/trailing OWS bytes tolerated
maxEmptyElements = 16 // number of empty list elements tolerated
)

func cutAtComma(s string, n int) (before, after string, found bool) {
// Note: this implementation draws inspiration from strings.Cut's.
end := min(len(s), n)
if i := strings.IndexByte(s[:end], ','); i >= 0 {
after = s[i+1:] // deal with this first to save one bounds check
return s[:i], after, true
}
return s, "", false
}

// TrimOWS trims up to n bytes of [optional whitespace (OWS)]
// from the start of and/or the end of s.
// If no more than n bytes of OWS are found at the start of s
// and no more than n bytes of OWS are found at the end of s,
// it returns the trimmed result and true.
// Otherwise, it returns the original string and false.
//
// [optional whitespace (OWS)]: https://httpwg.org/specs/rfc9110.html#whitespace
func trimOWS(s string, n int) (trimmed string, ok bool) {
if s == "" {
return s, true
}
trimmed, ok = trimRightOWS(s, n)
if !ok {
return s, false
}
posOfLastNameSeen := -1
chunkSize := set.maxLen + 1 // (to accommodate for at least one comma)
for {
// As a defense against maliciously long names in csv,
// we only process at most chunkSize bytes per iteration.
end := min(len(csv), chunkSize)
comma := strings.IndexByte(csv[:end], ',')
var name string
if comma == -1 {
name = csv
} else {
name = csv[:comma]
trimmed, ok = trimLeftOWS(trimmed, n)
if !ok {
return s, false
}
return trimmed, true
}

func trimLeftOWS(s string, n int) (string, bool) {
sCopy := s
var i int
for len(s) > 0 {
if i > n {
return sCopy, false
}
pos, found := set.m[name]
if !found {
return false
if !(s[0] == ' ' || s[0] == '\t') {
break
}
// The names in csv are expected to be sorted in lexicographical order
// and appear at most once in csv.
// Therefore, the positions (in set) of the names that
// appear in csv should form a strictly increasing sequence.
// If that's not actually the case, bail out.
if pos <= posOfLastNameSeen {
return false
s = s[1:]
i++
}
return s, true
}

func trimRightOWS(s string, n int) (string, bool) {
sCopy := s
var i int
for len(s) > 0 {
if i > n {
return sCopy, false
}
posOfLastNameSeen = pos
if comma < 0 { // We've now processed all the names in csv.
last := len(s) - 1
if !(s[last] == ' ' || s[last] == '\t') {
break
}
csv = csv[comma+1:]
s = s[:last]
i++
}
return true
return s, true
}

// TODO: when updating go directive to 1.21 or later,
Expand Down
Loading

0 comments on commit 441060e

Please sign in to comment.