cap /v2/token form body and skip multipart parsing
This commit is contained in:
+12
-5
@@ -186,7 +186,7 @@ func (b *BearerOIDC) RequireRegistryWrites(next http.Handler, realm func(*http.R
|
||||
hasAuth := r.Header.Get("Authorization") != ""
|
||||
switch {
|
||||
case r.URL.Path == "/v2/token":
|
||||
writeToken(w, b.registryToken(r))
|
||||
writeToken(w, b.registryToken(w, r))
|
||||
case r.URL.Path == "/v2/" && !hasAuth:
|
||||
unauthorized(w, challenge, "authentication required")
|
||||
default:
|
||||
@@ -198,12 +198,16 @@ func (b *BearerOIDC) RequireRegistryWrites(next http.Handler, realm func(*http.R
|
||||
// registryToken echoes a valid credential back as the registry token. Missing
|
||||
// or invalid credentials (e.g. an expired `docker login`) get the anonymous
|
||||
// token so pulls keep working; writes are still verified per request.
|
||||
// containerd/buildkit POST an OAuth2 password or refresh_token grant form.
|
||||
func (b *BearerOIDC) registryToken(r *http.Request) string {
|
||||
// containerd/buildkit POST an OAuth2 password or refresh_token grant form;
|
||||
// only a capped urlencoded body is read, never multipart.
|
||||
func (b *BearerOIDC) registryToken(w http.ResponseWriter, r *http.Request) string {
|
||||
raw := token(r)
|
||||
if raw == "" && r.Method == http.MethodPost {
|
||||
if raw = r.PostFormValue("password"); raw == "" {
|
||||
raw = r.PostFormValue("refresh_token")
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxTokenForm)
|
||||
if r.ParseForm() == nil {
|
||||
if raw = r.PostForm.Get("password"); raw == "" {
|
||||
raw = r.PostForm.Get("refresh_token")
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw == "" {
|
||||
@@ -216,6 +220,9 @@ func (b *BearerOIDC) registryToken(r *http.Request) string {
|
||||
return raw
|
||||
}
|
||||
|
||||
// maxTokenForm fits a large OIDC JWT plus the grant's other fields.
|
||||
const maxTokenForm = 16 << 10
|
||||
|
||||
func writeToken(w http.ResponseWriter, tok string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"token": tok, "access_token": tok})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -351,6 +352,25 @@ func TestRequireRegistryWrites(t *testing.T) {
|
||||
t.Errorf("oauth %s: %d %s", tc.name, w.Code, w.Body)
|
||||
}
|
||||
}
|
||||
var mp strings.Builder
|
||||
mw := multipart.NewWriter(&mp)
|
||||
_ = mw.WriteField("password", valid)
|
||||
_ = mw.Close()
|
||||
r := httptest.NewRequest(http.MethodPost, "/v2/token", strings.NewReader(mp.String()))
|
||||
r.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if !strings.Contains(w.Body.String(), `"token":"anonymous"`) || r.MultipartForm != nil {
|
||||
t.Errorf("multipart form honoured: %s", w.Body)
|
||||
}
|
||||
big := &countingReader{r: io.MultiReader(strings.NewReader("password="+valid+"&pad="), strings.NewReader(strings.Repeat("a", 1<<20)))}
|
||||
r = httptest.NewRequest(http.MethodPost, "/v2/token", big)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if !strings.Contains(w.Body.String(), `"token":"anonymous"`) || big.n > 2*maxTokenForm {
|
||||
t.Errorf("oversized form: read %d bytes, %s", big.n, w.Body)
|
||||
}
|
||||
for _, m := range []string{http.MethodGet, http.MethodHead} {
|
||||
if w := do(m, "/v2/repo/img/manifests/t", ""); w.Code != http.StatusNoContent {
|
||||
t.Errorf("anonymous %s: %d", m, w.Code)
|
||||
@@ -477,3 +497,14 @@ func TestRegistryContainerdClient(t *testing.T) {
|
||||
_ = rc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type countingReader struct {
|
||||
r io.Reader
|
||||
n int
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(p []byte) (int, error) {
|
||||
n, err := c.r.Read(p)
|
||||
c.n += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user