Skip to content

Commit 96ad8fa

Browse files
authored
feat: fail when the JSON input has extra fields (#211)
1 parent 7a1fa5d commit 96ad8fa

File tree

7 files changed

+215
-215
lines changed

7 files changed

+215
-215
lines changed

internal/common/validator.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/gofiber/fiber/v2"
1010

1111
"github.com/go-playground/validator/v10"
12+
13+
"github.com/italia/developers-italia-api/internal/jsondecoder"
1214
)
1315

1416
const (
@@ -63,7 +65,11 @@ func ValidateStruct(validateStruct interface{}) []ValidationError {
6365

6466
func ValidateRequestEntity(ctx *fiber.Ctx, request interface{}, errorMessage string) error {
6567
if err := ctx.BodyParser(request); err != nil {
66-
return Error(fiber.StatusBadRequest, errorMessage, "invalid json")
68+
if errors.Is(err, jsondecoder.ErrUnknownField) {
69+
return Error(fiber.StatusUnprocessableEntity, errorMessage, err.Error())
70+
}
71+
72+
return Error(fiber.StatusBadRequest, errorMessage, "invalid or malformed JSON")
6773
}
6874

6975
if err := ValidateStruct(request); err != nil {

internal/handlers/logs.go

+17-23
Original file line numberDiff line numberDiff line change
@@ -84,51 +84,47 @@ func (p *Log) GetLog(ctx *fiber.Ctx) error {
8484

8585
// PostLog creates a new log.
8686
func (p *Log) PostLog(ctx *fiber.Ctx) error {
87-
logReq := new(common.Log)
87+
const errMsg = "can't create Log"
8888

89-
if err := ctx.BodyParser(&logReq); err != nil {
90-
return common.Error(fiber.StatusBadRequest, "can't create Log", "invalid json")
91-
}
89+
logReq := new(common.Log)
9290

93-
if err := common.ValidateStruct(*logReq); err != nil {
94-
return common.ErrorWithValidationErrors(fiber.StatusUnprocessableEntity, "can't create Log", err)
91+
if err := common.ValidateRequestEntity(ctx, logReq, errMsg); err != nil {
92+
return err //nolint:wrapcheck
9593
}
9694

9795
log := models.Log{ID: utils.UUIDv4(), Message: logReq.Message}
9896

9997
if err := p.db.Create(&log).Error; err != nil {
100-
return common.Error(fiber.StatusInternalServerError, "can't create Log", "db error")
98+
return common.Error(fiber.StatusInternalServerError, errMsg, "db error")
10199
}
102100

103101
return ctx.JSON(&log)
104102
}
105103

106104
// PatchLog updates the log with the given ID.
107105
func (p *Log) PatchLog(ctx *fiber.Ctx) error {
108-
logReq := new(common.Log)
106+
const errMsg = "can't update Log"
109107

110-
if err := ctx.BodyParser(logReq); err != nil {
111-
return common.Error(fiber.StatusBadRequest, "can't update Log", "invalid json")
112-
}
108+
logReq := new(common.Log)
113109

114-
if err := common.ValidateStruct(*logReq); err != nil {
115-
return common.ErrorWithValidationErrors(fiber.StatusUnprocessableEntity, "can't update Log", err)
110+
if err := common.ValidateRequestEntity(ctx, logReq, errMsg); err != nil {
111+
return err //nolint:wrapcheck
116112
}
117113

118114
log := models.Log{}
119115

120116
if err := p.db.First(&log, "id = ?", ctx.Params("id")).Error; err != nil {
121117
if errors.Is(err, gorm.ErrRecordNotFound) {
122-
return common.Error(fiber.StatusNotFound, "can't update Log", "Log was not found")
118+
return common.Error(fiber.StatusNotFound, errMsg, "Log was not found")
123119
}
124120

125-
return common.Error(fiber.StatusInternalServerError, "can't update Log", "internal server error")
121+
return common.Error(fiber.StatusInternalServerError, errMsg, "internal server error")
126122
}
127123

128124
log.Message = logReq.Message
129125

130126
if err := p.db.Updates(&log).Error; err != nil {
131-
return common.Error(fiber.StatusInternalServerError, "can't update Log", "db error")
127+
return common.Error(fiber.StatusInternalServerError, errMsg, "db error")
132128
}
133129

134130
return ctx.JSON(&log)
@@ -198,6 +194,8 @@ func (p *Log) GetSoftwareLogs(ctx *fiber.Ctx) error {
198194

199195
// PostSoftwareLog creates a new log associated to a Software with the given ID and returns any error encountered.
200196
func (p *Log) PostSoftwareLog(ctx *fiber.Ctx) error {
197+
const errMsg = "can't create Log"
198+
201199
logReq := new(common.Log)
202200

203201
software := models.Software{}
@@ -213,12 +211,8 @@ func (p *Log) PostSoftwareLog(ctx *fiber.Ctx) error {
213211
)
214212
}
215213

216-
if err := ctx.BodyParser(&logReq); err != nil {
217-
return common.Error(fiber.StatusBadRequest, "can't create Log", "invalid json")
218-
}
219-
220-
if err := common.ValidateStruct(*logReq); err != nil {
221-
return common.ErrorWithValidationErrors(fiber.StatusUnprocessableEntity, "can't create Log", err)
214+
if err := common.ValidateRequestEntity(ctx, logReq, errMsg); err != nil {
215+
return err //nolint:wrapcheck
222216
}
223217

224218
table := models.Software{}.TableName()
@@ -231,7 +225,7 @@ func (p *Log) PostSoftwareLog(ctx *fiber.Ctx) error {
231225
}
232226

233227
if err := p.db.Create(&log).Error; err != nil {
234-
return common.Error(fiber.StatusInternalServerError, "can't create Log", "db error")
228+
return common.Error(fiber.StatusInternalServerError, errMsg, "db error")
235229
}
236230

237231
return ctx.JSON(&log)

internal/handlers/software.go

+10-18
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,12 @@ func (p *Software) GetSoftware(ctx *fiber.Ctx) error {
149149

150150
// PostSoftware creates a new software.
151151
func (p *Software) PostSoftware(ctx *fiber.Ctx) error {
152-
softwareReq := new(common.SoftwarePost)
152+
const errMsg = "can't create Software"
153153

154-
if err := ctx.BodyParser(&softwareReq); err != nil {
155-
return common.Error(fiber.StatusBadRequest, "can't create Software", "invalid json")
156-
}
154+
softwareReq := new(common.SoftwarePost)
157155

158-
if err := common.ValidateStruct(*softwareReq); err != nil {
159-
return common.ErrorWithValidationErrors(
160-
fiber.StatusUnprocessableEntity, "can't create Software", err,
161-
)
156+
if err := common.ValidateRequestEntity(ctx, softwareReq, errMsg); err != nil {
157+
return err //nolint:wrapcheck
162158
}
163159

164160
aliases := []models.SoftwareURL{}
@@ -180,14 +176,16 @@ func (p *Software) PostSoftware(ctx *fiber.Ctx) error {
180176
}
181177

182178
if err := p.db.Create(&software).Error; err != nil {
183-
return common.Error(fiber.StatusInternalServerError, "can't create Software", err.Error())
179+
return common.Error(fiber.StatusInternalServerError, errMsg, err.Error())
184180
}
185181

186182
return ctx.JSON(&software)
187183
}
188184

189185
// PatchSoftware updates the software with the given ID.
190-
func (p *Software) PatchSoftware(ctx *fiber.Ctx) error { //nolint:cyclop // mostly error handling ifs
186+
func (p *Software) PatchSoftware(ctx *fiber.Ctx) error {
187+
const errMsg = "can't update Software"
188+
191189
softwareReq := new(common.SoftwarePatch)
192190
software := models.Software{}
193191

@@ -202,14 +200,8 @@ func (p *Software) PatchSoftware(ctx *fiber.Ctx) error { //nolint:cyclop // most
202200
return common.Error(fiber.StatusInternalServerError, "can't update Software", "internal server error")
203201
}
204202

205-
if err := ctx.BodyParser(softwareReq); err != nil {
206-
return common.Error(fiber.StatusBadRequest, "can't update Software", "invalid json")
207-
}
208-
209-
if err := common.ValidateStruct(*softwareReq); err != nil {
210-
return common.ErrorWithValidationErrors(
211-
fiber.StatusUnprocessableEntity, "can't update Software", err,
212-
)
203+
if err := common.ValidateRequestEntity(ctx, softwareReq, errMsg); err != nil {
204+
return err //nolint:wrapcheck
213205
}
214206

215207
// Slice of urls that we expect in the database after the PATCH (url + aliases)

internal/handlers/webhooks.go

+17-29
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,14 @@ func (p *Webhook[T]) GetSingleResourceWebhooks(ctx *fiber.Ctx) error {
114114
// PostSingleResourceWebhook creates a new webhook associated to resources
115115
// (fe. Software, Publishers) and returns any error encountered.
116116
func (p *Webhook[T]) PostResourceWebhook(ctx *fiber.Ctx) error {
117+
const errMsg = "can't create Webhook"
118+
117119
webhookReq := new(common.Webhook)
118120

119121
var resource T
120122

121-
if err := ctx.BodyParser(&webhookReq); err != nil {
122-
return common.Error(fiber.StatusBadRequest, "can't create Webhook", "invalid json")
123-
}
124-
125-
if err := common.ValidateStruct(*webhookReq); err != nil {
126-
return common.ErrorWithValidationErrors(
127-
fiber.StatusUnprocessableEntity, "can't create Webhook", err,
128-
)
123+
if err := common.ValidateRequestEntity(ctx, webhookReq, errMsg); err != nil {
124+
return err //nolint:wrapcheck
129125
}
130126

131127
webhook := models.Webhook{
@@ -137,7 +133,7 @@ func (p *Webhook[T]) PostResourceWebhook(ctx *fiber.Ctx) error {
137133
}
138134

139135
if err := p.db.Create(&webhook).Error; err != nil {
140-
return common.Error(fiber.StatusInternalServerError, "can't create Webhook", "db error")
136+
return common.Error(fiber.StatusInternalServerError, errMsg, "db error")
141137
}
142138

143139
return ctx.JSON(&webhook)
@@ -146,6 +142,8 @@ func (p *Webhook[T]) PostResourceWebhook(ctx *fiber.Ctx) error {
146142
// PostResourceWebhook creates a new webhook associated to a resource with the given ID
147143
// (fe. a specific Software or Publisher) and returns any error encountered.
148144
func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error {
145+
const errMsg = "can't create Webhook"
146+
149147
webhookReq := new(common.Webhook)
150148

151149
var resource T
@@ -162,14 +160,8 @@ func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error {
162160
)
163161
}
164162

165-
if err := ctx.BodyParser(&webhookReq); err != nil {
166-
return common.Error(fiber.StatusBadRequest, "can't create Webhook", "invalid json")
167-
}
168-
169-
if err := common.ValidateStruct(*webhookReq); err != nil {
170-
return common.ErrorWithValidationErrors(
171-
fiber.StatusUnprocessableEntity, "can't create Webhook", err,
172-
)
163+
if err := common.ValidateRequestEntity(ctx, webhookReq, errMsg); err != nil {
164+
return err //nolint:wrapcheck
173165
}
174166

175167
webhook := models.Webhook{
@@ -181,44 +173,40 @@ func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error {
181173
}
182174

183175
if err := p.db.Create(&webhook).Error; err != nil {
184-
return common.Error(fiber.StatusInternalServerError, "can't create Webhook", "db error")
176+
return common.Error(fiber.StatusInternalServerError, errMsg, "db error")
185177
}
186178

187179
return ctx.JSON(&webhook)
188180
}
189181

190182
// PatchWebhook updates the webhook with the given ID.
191183
func (p *Webhook[T]) PatchWebhook(ctx *fiber.Ctx) error {
192-
webhookReq := new(common.Webhook)
184+
const errMsg = "can't update Webhook"
193185

194-
if err := ctx.BodyParser(webhookReq); err != nil {
195-
return common.Error(fiber.StatusBadRequest, "can't update Webhook", "invalid json")
196-
}
186+
webhookReq := new(common.Webhook)
197187

198-
if err := common.ValidateStruct(*webhookReq); err != nil {
199-
return common.ErrorWithValidationErrors(
200-
fiber.StatusUnprocessableEntity, "can't update Webhook", err,
201-
)
188+
if err := common.ValidateRequestEntity(ctx, webhookReq, errMsg); err != nil {
189+
return err //nolint:wrapcheck
202190
}
203191

204192
webhook := models.Webhook{}
205193

206194
if err := p.db.First(&webhook, "id = ?", ctx.Params("id")).Error; err != nil {
207195
if errors.Is(err, gorm.ErrRecordNotFound) {
208-
return common.Error(fiber.StatusNotFound, "can't update Webhook", "Webhook was not found")
196+
return common.Error(fiber.StatusNotFound, errMsg, "Webhook was not found")
209197
}
210198

211199
return common.Error(
212200
fiber.StatusInternalServerError,
213-
"can't update Webhook",
201+
errMsg,
214202
fiber.ErrInternalServerError.Message,
215203
)
216204
}
217205

218206
webhook.URL = webhookReq.URL
219207

220208
if err := p.db.Updates(&webhook).Error; err != nil {
221-
return common.Error(fiber.StatusInternalServerError, "can't update Webhook", "db error")
209+
return common.Error(fiber.StatusInternalServerError, errMsg, "db error")
222210
}
223211

224212
return ctx.JSON(&webhook)

internal/jsondecoder/jsondecoder.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package jsondecoder
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"strings"
8+
)
9+
10+
var (
11+
ErrExtraDataAfterDecoding = errors.New("extra data after decoding")
12+
ErrUnknownField = errors.New("unknown field in JSON input")
13+
)
14+
15+
// UnmarshalDisallowUnknownFieldsUnmarshal parses the JSON-encoded data
16+
// and stores the result in the value pointed to by v like json.Unmarshal,
17+
// but with DisallowUnknownFields() set by default for extra security.
18+
func UnmarshalDisallowUnknownFields(data []byte, v interface{}) error {
19+
dec := json.NewDecoder(bytes.NewReader(data))
20+
dec.DisallowUnknownFields()
21+
22+
if err := dec.Decode(v); err != nil {
23+
// Ugly, but the encoding/json uses a dynamic error here
24+
if strings.HasPrefix(err.Error(), "json: unknown field ") {
25+
return ErrUnknownField
26+
}
27+
28+
// we want to provide an alternative implementation, with the
29+
// unwrapped errors
30+
//nolint:wrapcheck
31+
return err
32+
}
33+
34+
// Check if there's any data left in the decoder's buffer.
35+
// This ensures that there's no extra JSON after the main object
36+
// otherwise something like '{"foo": 1}{"bar": 2}' or even '{}garbage'
37+
// will not error out.
38+
if dec.More() {
39+
return ErrExtraDataAfterDecoding
40+
}
41+
42+
return nil
43+
}

main.go

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/italia/developers-italia-api/internal/common"
1212
"github.com/italia/developers-italia-api/internal/database"
1313
"github.com/italia/developers-italia-api/internal/handlers"
14+
"github.com/italia/developers-italia-api/internal/jsondecoder"
1415
"github.com/italia/developers-italia-api/internal/middleware"
1516
"github.com/italia/developers-italia-api/internal/models"
1617
"github.com/italia/developers-italia-api/internal/webhooks"
@@ -54,6 +55,9 @@ func Setup() *fiber.App {
5455

5556
app := fiber.New(fiber.Config{
5657
ErrorHandler: common.CustomErrorHandler,
58+
// Fiber doesn't set DisallowUnknownFields by default
59+
// (https://github.com/gofiber/fiber/issues/2601)
60+
JSONDecoder: jsondecoder.UnmarshalDisallowUnknownFields,
5761
})
5862

5963
// Automatically recover panics in handlers

0 commit comments

Comments
 (0)