Skip to content

c.bind() binds both POST request body and query to given struct causing security vulnerability and problems with slices #1670

@aldas

Description

@aldas

Issue Description

Commit b129098 removed content length and request method check from c.bind() function. This causes many unwanted side effects

  1. security vulnerability - now it is possible to inject field values from query param to POST method body with bind. This could allow attacker to inject to fields that are not meant to be filled from bind. I know that you should not pass binded values directly to services etc but before this change this was not possible with query params. It is way easier to manipulate query params than request body.

see example:

func TestSecurityRisk(t *testing.T) {
    type User struct {
        ID int `json:"id"`
        IsAdmin bool // not 'exposed' value should not be bind DEFINITELY from URL/ROUTE parameters
    }

    e := echo.New()
    req := httptest.NewRequest(http.MethodPost, "/api/endpoint?IsAdmin=true", strings.NewReader(`{"id": 1}`))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    err := func(c echo.Context) error {
        var payload User
        if err := c.Bind(&payload); err != nil {
            return c.JSON(http.StatusBadRequest, echo.Map{"error": err})
        }

        // passing bind value directly to service is bad practice and could lead to security risks but c.bind()
        // doing it implicitly is also very bad (even worse)

        // err := service.addUser(c.Request().Context(), user)

        if payload.IsAdmin {
            panic("passing bind value directly to service is bad practice and could lead to security risks")
        }

        return c.JSON(http.StatusOK, payload)
    }(c)
    if err != nil {
        t.Fatal(err)
    }
}
  1. if request contains query param + body and we are binding to slice request fails with binding element must be a struct

Example:

func TestBindingToSliceWithQueryParam(t *testing.T) {
	type User struct {
		ID int `json:"id"`
	}

	e := echo.New()
	req := httptest.NewRequest(http.MethodPost, "/api/endpoint?lang=et", strings.NewReader(`[{"id": 1}]`))
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	err := func(c echo.Context) error {
		var payload []User
		if err := c.Bind(&payload); err != nil {
			return c.JSON(http.StatusBadRequest, echo.Map{"error": err})
		}

		return c.JSON(http.StatusOK, payload)
	}(c)
	if err != nil {
		t.Fatal(err)
	}
	response := string(rec.Body.Bytes())
	if strings.Contains(response, "binding element must be a struct"){
		t.Fatal(response)
	}
}

Expected behaviour

I think expected behavior is as it was before b129098

Version/commit

v4.1.17

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions