Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Retrieve it:
curl http://localhost:8080/orders/ord-000001 -H 'X-User-ID: user-123'
```

Check service health with `curl http://localhost:8080/healthz`.

## Run DiffPal in your own repository

1. Create a repository from this template.
Expand Down
11 changes: 5 additions & 6 deletions internal/httpapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

type createOrderRequest struct {
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
UnitPriceCents int `json:"unit_price_cents"`
}

func (h *Handler) createOrder(w http.ResponseWriter, r *http.Request) {
Expand All @@ -49,6 +50,7 @@ func (h *Handler) createOrder(w http.ResponseWriter, r *http.Request) {
}
order, err := h.service.Create(r.Context(), orders.CreateInput{
UserID: userID, ProductID: request.ProductID, Quantity: request.Quantity,
UnitPriceCents: request.UnitPriceCents,
})
if err != nil {
switch {
Expand Down Expand Up @@ -77,10 +79,6 @@ func (h *Handler) getOrder(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, "could not load order")
return
}
if order.UserID != r.Header.Get("X-User-ID") {
writeError(w, http.StatusNotFound, "order not found")
return
Comment on lines -80 to -82

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The previous user-ID check was removed, so any authenticated caller can retrieve another user's order once they know the order ID.

  • Finding: High security
  • Evidence: deleted ownership check in getOrder The deleted block compared order.UserID to X-User-ID and returned 404 on mismatch. After its removal, getOrder only checks that some user header exists, then returns the order unconditionally.
  • Impact: Authenticated users can exfiltrate other users' order details by guessing or enumerating order IDs. Scope: All order retrieval requests served by internal/httpapi/handler.go.
  • Suggestion: Restore the owner check before writing the order response.

}
writeJSON(w, http.StatusOK, order)
}

Expand All @@ -95,6 +93,7 @@ func requestingUser(w http.ResponseWriter, r *http.Request) (string, bool) {

func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
Expand Down
7 changes: 4 additions & 3 deletions internal/orders/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type Order struct {
}

type CreateInput struct {
UserID string
ProductID string
Quantity int
UserID string
ProductID string
Quantity int
UnitPriceCents int
}
7 changes: 4 additions & 3 deletions internal/orders/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func (s *Service) Create(ctx context.Context, input CreateInput) (Order, error)
if !ok {
return Order{}, ErrInvalidProduct
}
if input.UnitPriceCents > 0 {
unitPrice = input.UnitPriceCents
Comment on lines +39 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Create now replaces the catalog price with input.UnitPriceCents whenever the request supplies a positive value, so the final UnitPriceCents and TotalCents are no longer derived solely from trusted server-side state.

  • Finding: High correctness
  • Evidence: unitPrice override from input.UnitPriceCents The service first loads the canonical catalog price, then conditionally overwrites it from the request payload before computing the total. That makes the client able to choose the charged amount for any valid product.
  • Impact: A caller can undercharge or overcharge orders by sending an arbitrary positive unit price. Scope: Order creation and total calculation in the checkout flow.
  • Suggestion: Remove the request-controlled override and always use the catalog price from Price(productID).

}

s.mu.Lock()
s.next++
Expand All @@ -51,9 +54,7 @@ func (s *Service) Create(ctx context.Context, input CreateInput) (Order, error)
TotalCents: unitPrice * input.Quantity,
CreatedAt: s.now().UTC(),
}
if err := s.repo.Save(ctx, order); err != nil {
return Order{}, err
}
s.repo.Save(ctx, order)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Create ignores the return value from s.repo.Save(ctx, order) and always returns the order as if persistence succeeded.

  • Finding: Medium reliability
  • Evidence: unassigned s.repo.Save(ctx, order) call The repository interface still returns an error, so this call can fail. Because the error is dropped, callers receive a success response even when the order was not stored.
  • Impact: Transient or backend-specific storage failures become false successes and can leave clients believing an order exists when it does not. Scope: Any repository implementation that can return a save error.
  • Suggestion: Capture the Save error and return it to the caller.

return order, nil
}

Expand Down
Loading