forked from wrenn/wrenn
Fix LIKE pattern injection in user email search
Escape LIKE metacharacters (% and _) in the email prefix before passing to the SQL query, and enforce the documented '@' requirement to prevent broad user enumeration. Move search logic out of TeamService into usersHandler since it is a site-wide lookup, not team-scoped.
This commit is contained in:
@ -4,34 +4,38 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/auth"
|
"git.omukk.dev/wrenn/sandbox/internal/auth"
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/service"
|
"git.omukk.dev/wrenn/sandbox/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type usersHandler struct {
|
type usersHandler struct {
|
||||||
svc *service.TeamService
|
db *db.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUsersHandler(svc *service.TeamService) *usersHandler {
|
func newUsersHandler(db *db.Queries) *usersHandler {
|
||||||
return &usersHandler{svc: svc}
|
return &usersHandler{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search handles GET /v1/users/search?email=<prefix>
|
// Search handles GET /v1/users/search?email=<prefix>
|
||||||
// Returns up to 10 users whose email starts with the given prefix.
|
// Returns up to 10 users whose email starts with the given prefix.
|
||||||
// The prefix must be at least 3 characters long.
|
// The prefix must be at least 3 characters long and contain "@".
|
||||||
func (h *usersHandler) Search(w http.ResponseWriter, r *http.Request) {
|
func (h *usersHandler) Search(w http.ResponseWriter, r *http.Request) {
|
||||||
auth.MustFromContext(r.Context()) // ensure authenticated
|
auth.MustFromContext(r.Context()) // ensure authenticated
|
||||||
|
|
||||||
prefix := strings.TrimSpace(r.URL.Query().Get("email"))
|
prefix := strings.TrimSpace(r.URL.Query().Get("email"))
|
||||||
if len(prefix) < 3 {
|
if len(prefix) < 3 || !strings.Contains(prefix, "@") {
|
||||||
writeError(w, http.StatusBadRequest, "invalid_request", "email prefix must be at least 3 characters")
|
writeError(w, http.StatusBadRequest, "invalid_request", "email prefix must be at least 3 characters and contain '@'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := h.svc.SearchUsersByEmailPrefix(r.Context(), prefix)
|
// Escape LIKE metacharacters to prevent pattern injection.
|
||||||
|
escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(prefix)
|
||||||
|
|
||||||
|
results, err := h.db.SearchUsersByEmailPrefix(r.Context(), pgtype.Text{String: escaped, Valid: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status, code, msg := serviceErrToHTTP(err)
|
writeError(w, http.StatusInternalServerError, "internal", "search failed")
|
||||||
writeError(w, status, code, msg)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ func New(
|
|||||||
apiKeys := newAPIKeyHandler(apiKeySvc, al)
|
apiKeys := newAPIKeyHandler(apiKeySvc, al)
|
||||||
hostH := newHostHandler(hostSvc, queries, al)
|
hostH := newHostHandler(hostSvc, queries, al)
|
||||||
teamH := newTeamHandler(teamSvc, al)
|
teamH := newTeamHandler(teamSvc, al)
|
||||||
usersH := newUsersHandler(teamSvc)
|
usersH := newUsersHandler(queries)
|
||||||
auditH := newAuditHandler(auditSvc)
|
auditH := newAuditHandler(auditSvc)
|
||||||
statsH := newStatsHandler(statsSvc)
|
statsH := newStatsHandler(statsSvc)
|
||||||
metricsH := newSandboxMetricsHandler(queries, pool)
|
metricsH := newSandboxMetricsHandler(queries, pool)
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/sandbox/internal/db"
|
"git.omukk.dev/wrenn/sandbox/internal/db"
|
||||||
@ -369,12 +368,6 @@ func (s *TeamService) LeaveTeam(ctx context.Context, teamID, callerUserID string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUsersByEmailPrefix returns up to 10 users whose email starts with the given prefix.
|
|
||||||
// The prefix must contain "@" to prevent broad enumeration.
|
|
||||||
func (s *TeamService) SearchUsersByEmailPrefix(ctx context.Context, prefix string) ([]db.SearchUsersByEmailPrefixRow, error) {
|
|
||||||
return s.DB.SearchUsersByEmailPrefix(ctx, pgtype.Text{String: prefix, Valid: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBYOC enables the BYOC feature flag for a team. Once enabled, BYOC cannot
|
// SetBYOC enables the BYOC feature flag for a team. Once enabled, BYOC cannot
|
||||||
// be disabled — it is a one-way transition.
|
// be disabled — it is a one-way transition.
|
||||||
// Admin-only — the caller must verify admin status before invoking this.
|
// Admin-only — the caller must verify admin status before invoking this.
|
||||||
|
|||||||
Reference in New Issue
Block a user