mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-20 03:46:29 +00:00
fix: explicitly require JSON for request bodies
This commit is contained in:
@@ -3,8 +3,10 @@ package httpserver
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
@@ -16,6 +18,10 @@ import (
|
||||
|
||||
// BindJSON binds and normalizes a JSON request while distinguishing invalid input from internal failures
|
||||
func BindJSON(c *gin.Context, value any) error {
|
||||
if err := requireJSONContentType(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := classifyBindingError(c.ShouldBindJSON(value))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -27,6 +33,13 @@ func BindJSON(c *gin.Context, value any) error {
|
||||
|
||||
// BindOptionalJSON accepts an empty body while normalizing valid input and classifying malformed JSON as invalid input
|
||||
func BindOptionalJSON(c *gin.Context, value any) error {
|
||||
if c.Request.ContentLength == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := requireJSONContentType(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := c.ShouldBindJSON(value)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
@@ -52,6 +65,31 @@ func FormFile(c *gin.Context, field string) (*multipart.FileHeader, error) {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func requireJSONContentType(c *gin.Context) error {
|
||||
mediaType, _, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
|
||||
if err != nil {
|
||||
return apperror.InvalidRequestBody(err)
|
||||
}
|
||||
if !isJSONMediaType(mediaType) {
|
||||
return apperror.InvalidRequestBody(errors.New("request Content-Type is not JSON"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isJSONMediaType(mediaType string) bool {
|
||||
topLevelType, subtype, ok := strings.Cut(mediaType, "/")
|
||||
if !ok || topLevelType != "application" {
|
||||
return false
|
||||
}
|
||||
if subtype == "json" {
|
||||
return true
|
||||
}
|
||||
|
||||
baseSubtype, ok := strings.CutSuffix(subtype, "+json")
|
||||
return ok && baseSubtype != ""
|
||||
}
|
||||
|
||||
func classifyBindingError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
|
||||
@@ -63,6 +63,40 @@ func TestBindJSONNormalizesTaggedFieldsRecursively(t *testing.T) {
|
||||
require.Equal(t, norm.NFC.String("Résumé"), input.Items[0].Label)
|
||||
}
|
||||
|
||||
func TestBindJSONRejectsFormCompatibleContentTypes(t *testing.T) {
|
||||
for _, contentType := range []string{"text/plain", "application/x-www-form-urlencoded", "multipart/form-data; boundary=test"} {
|
||||
t.Run(contentType, func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(`{"name":"admin"}`))
|
||||
c.Request.Header.Set("Content-Type", contentType)
|
||||
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
err := BindJSON(c, &input)
|
||||
|
||||
require.True(t, apperror.IsCode(err, apperror.CodeInvalidRequestBody))
|
||||
require.Empty(t, input.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindJSONAcceptsStructuredJSONContentType(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/", strings.NewReader(`{"name":"admin"}`))
|
||||
c.Request.Header.Set("Content-Type", "application/scim+json")
|
||||
|
||||
var input struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
err := BindJSON(c, &input)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "admin", input.Name)
|
||||
}
|
||||
|
||||
func TestFormFileClassifiesMissingField(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
|
||||
Reference in New Issue
Block a user