Harden CIMD client resolution and caching

Tighten redirect URI validation for metadata documents, honor
Cache-Control no-store when caching fetched documents, and resolve
clients on the same transaction as authorization. Load
external_client_id from the database and parse unbounded max-stale
directives in cachecontrol.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-06-19 16:51:30 +02:00
parent 5b0d3e5052
commit d7e23fd890
7 changed files with 257 additions and 54 deletions

View File

@@ -137,6 +137,11 @@ func ParseRequest(header string) (*RequestDirective, error) {
dir.maxAge = &seconds
case MaxStale:
if token.Value == "" {
dir.maxStaleUnbounded = true
break
}
seconds, err := parseDeltaSeconds(token.Value)
if err != nil {
return nil, fmt.Errorf("cannot parse max-stale: %w", err)
@@ -292,6 +297,10 @@ func parseDirectives(header string, parse func(string) (*TokenPair, error)) ([]*
tokens = append(tokens, token)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("cannot scan cache-control directives: %w", err)
}
return tokens, nil
}
@@ -370,6 +379,7 @@ func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
@@ -377,10 +387,12 @@ func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte
}
var ws int
inQuotes := false
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
switch {

View File

@@ -172,6 +172,37 @@ func TestParseRequest(t *testing.T) {
require.Error(t, err)
},
)
t.Run(
"max-stale without value",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseRequest("max-stale")
require.NoError(t, err)
assert.True(t, dir.MaxStaleUnbounded())
_, bounded, ok := dir.MaxStale()
require.True(t, ok)
assert.False(t, bounded)
},
)
t.Run(
"max-stale with value",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseRequest("max-stale=120")
require.NoError(t, err)
assert.False(t, dir.MaxStaleUnbounded())
seconds, bounded, ok := dir.MaxStale()
require.True(t, ok)
assert.True(t, bounded)
assert.Equal(t, uint64(120), seconds)
},
)
}
func TestParseResponse(t *testing.T) {
@@ -378,10 +409,12 @@ func TestResponseMaxAgeDuration(t *testing.T) {
_, gotOK := dir.MaxAgeDuration()
assert.False(t, gotOK)
}
return
}
require.NoError(t, err)
gotAge, gotOK := dir.MaxAgeDuration()
assert.True(t, gotOK)
assert.Equal(t, tt.wantAge, gotAge)

View File

@@ -18,14 +18,15 @@ import "time"
type (
RequestDirective struct {
maxAge *uint64
maxStale *uint64
minFresh *uint64
noCache bool
noStore bool
noTransform bool
onlyIfCached bool
extensions map[string]string
maxAge *uint64
maxStale *uint64
maxStaleUnbounded bool
minFresh *uint64
noCache bool
noStore bool
noTransform bool
onlyIfCached bool
extensions map[string]string
}
ResponseDirective struct {
@@ -50,12 +51,20 @@ func (d *RequestDirective) MaxAge() (uint64, bool) {
return 0, false
}
func (d *RequestDirective) MaxStale() (uint64, bool) {
if v := d.maxStale; v != nil {
return *v, true
func (d *RequestDirective) MaxStale() (seconds uint64, bounded bool, ok bool) {
if d.maxStaleUnbounded {
return 0, false, true
}
return 0, false
if v := d.maxStale; v != nil {
return *v, true, true
}
return 0, false, false
}
func (d *RequestDirective) MaxStaleUnbounded() bool {
return d.maxStaleUnbounded
}
func (d *RequestDirective) MinFresh() (uint64, bool) {