Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions backend/app/api/handlers/v1/v1_ctrl_items.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ import (
// @Summary Query All Items
// @Tags Items
// @Produce json
// @Param q query string false "search string"
// @Param page query int false "page number"
// @Param pageSize query int false "items per page"
// @Param tags query []string false "tags Ids" collectionFormat(multi)
// @Param locations query []string false "location Ids" collectionFormat(multi)
// @Param parentIds query []string false "parent Ids" collectionFormat(multi)
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Param q query string false "Search string"
// @Param page query int false "Page number"
// @Param pageSize query int false "Items per page"
// @Param tags query []string false "Tag Ids to filter the results by" collectionFormat(multi)
// @Param negateTags query bool false "Exclude tags specified in the query parameter"
// @Param locations query []string false "Location Ids to filter the results by" collectionFormat(multi)
// @Param parentIds query []string false "Parent Ids to filter the results by" collectionFormat(multi)
// @Param onlyWithPhoto query bool false "Only return items that have a photo"
// @Param onlyWithoutPhoto query bool false "Only return items that don't have a photo"
// @Param includeArchived query bool false "Include items in the results that have been archived"
// @Param orderBy query string false "Field to order the results by"
// @Param orderByDirection query string false "Direction to order the results by"
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
Expand Down Expand Up @@ -65,6 +71,7 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
IncludeArchived: queryBool(params.Get("includeArchived")),
Fields: filterFieldItems(params["fields"]),
OrderBy: params.Get("orderBy"),
OrderByDirection: params.Get("orderByDirection"),
}
Comment on lines 73 to 75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden sorting query params with an allowlist before passing to repository.

orderByDirection is accepted as raw input and forwarded directly. Please normalize/validate orderBy and orderByDirection (asc/desc) at the handler boundary and reject invalid values with 400 instead of silently coercing them. This is a security hardening and behavior-consistency improvement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/handlers/v1/v1_ctrl_items.go` around lines 73 - 75, Validate
and normalize the incoming sorting params at the handler boundary: check
params.Get("orderBy") against a predefined allowlist of permitted column names
(e.g., the set of sortable fields used by the repository) and only accept
params.Get("orderByDirection") if it normalizes to "asc" or "desc"
(case-insensitive); if either is invalid, return an HTTP 400 error from the
handler instead of passing raw values into the query struct (the fields OrderBy
and OrderByDirection) that is sent to the repository. Ensure you normalize the
accepted direction to a consistent lowercase value before constructing the query
and reference the same allowlist used by the repository to avoid mismatches.


if strings.HasPrefix(v.Search, "#") {
Expand Down
48 changes: 42 additions & 6 deletions backend/app/api/static/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,19 +707,19 @@ const docTemplate = `{
"parameters": [
{
"type": "string",
"description": "search string",
"description": "Search string",
"name": "q",
"in": "query"
},
{
"type": "integer",
"description": "page number",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "items per page",
"description": "Items per page",
"name": "pageSize",
"in": "query"
},
Expand All @@ -729,17 +729,23 @@ const docTemplate = `{
"type": "string"
},
"collectionFormat": "multi",
"description": "tags Ids",
"description": "Tag Ids to filter the results by",
"name": "tags",
"in": "query"
},
{
"type": "boolean",
"description": "Exclude tags specified in the query parameter",
"name": "negateTags",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "location Ids",
"description": "Location Ids to filter the results by",
"name": "locations",
"in": "query"
},
Expand All @@ -749,9 +755,39 @@ const docTemplate = `{
"type": "string"
},
"collectionFormat": "multi",
"description": "parent Ids",
"description": "Parent Ids to filter the results by",
"name": "parentIds",
"in": "query"
},
{
"type": "boolean",
"description": "Only return items that have a photo",
"name": "onlyWithPhoto",
"in": "query"
},
{
"type": "boolean",
"description": "Only return items that don't have a photo",
"name": "onlyWithoutPhoto",
"in": "query"
},
{
"type": "boolean",
"description": "Include items in the results that have been archived",
"name": "includeArchived",
"in": "query"
},
{
"type": "string",
"description": "Field to order the results by",
"name": "orderBy",
"in": "query"
},
{
"type": "string",
"description": "Direction to order the results by",
"name": "orderByDirection",
"in": "query"
}
],
"responses": {
Expand Down
60 changes: 54 additions & 6 deletions backend/app/api/static/docs/openapi-3.json
Original file line number Diff line number Diff line change
Expand Up @@ -715,31 +715,31 @@
"summary": "Query All Items",
"parameters": [
{
"description": "search string",
"description": "Search string",
"name": "q",
"in": "query",
"schema": {
"type": "string"
}
},
{
"description": "page number",
"description": "Page number",
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"description": "items per page",
"description": "Items per page",
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"description": "tags Ids",
"description": "Tag Ids to filter the results by",
"name": "tags",
"in": "query",
"explode": true,
Expand All @@ -751,7 +751,15 @@
}
},
{
"description": "location Ids",
"description": "Exclude tags specified in the query parameter",
"name": "negateTags",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"description": "Location Ids to filter the results by",
"name": "locations",
"in": "query",
"explode": true,
Expand All @@ -763,7 +771,7 @@
}
},
{
"description": "parent Ids",
"description": "Parent Ids to filter the results by",
"name": "parentIds",
"in": "query",
"explode": true,
Expand All @@ -773,6 +781,46 @@
"type": "string"
}
}
},
{
"description": "Only return items that have a photo",
"name": "onlyWithPhoto",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"description": "Only return items that don't have a photo",
"name": "onlyWithoutPhoto",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"description": "Include items in the results that have been archived",
"name": "includeArchived",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"description": "Field to order the results by",
"name": "orderBy",
"in": "query",
"schema": {
"type": "string"
}
},
{
"description": "Direction to order the results by",
"name": "orderByDirection",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
Expand Down
42 changes: 36 additions & 6 deletions backend/app/api/static/docs/openapi-3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -416,45 +416,75 @@ paths:
- Items
summary: Query All Items
parameters:
- description: search string
- description: Search string
name: q
in: query
schema:
type: string
- description: page number
- description: Page number
name: page
in: query
schema:
type: integer
- description: items per page
- description: Items per page
name: pageSize
in: query
schema:
type: integer
- description: tags Ids
- description: Tag Ids to filter the results by
name: tags
in: query
explode: true
schema:
type: array
items:
type: string
- description: location Ids
- description: Exclude tags specified in the query parameter
name: negateTags
in: query
schema:
type: boolean
- description: Location Ids to filter the results by
name: locations
in: query
explode: true
schema:
type: array
items:
type: string
- description: parent Ids
- description: Parent Ids to filter the results by
name: parentIds
in: query
explode: true
schema:
type: array
items:
type: string
- description: Only return items that have a photo
name: onlyWithPhoto
in: query
schema:
type: boolean
- description: Only return items that don't have a photo
name: onlyWithoutPhoto
in: query
schema:
type: boolean
- description: Include items in the results that have been archived
name: includeArchived
in: query
schema:
type: boolean
- description: Field to order the results by
name: orderBy
in: query
schema:
type: string
- description: Direction to order the results by
name: orderByDirection
in: query
schema:
type: string
responses:
"200":
description: OK
Expand Down
Loading
Loading