Compare commits

..

11 Commits
v1.0.3 ... main

Author SHA1 Message Date
Hein
82d84435f2 Fixed version release auto inc
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -25m36s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -25m5s
Build , Vet Test, and Lint / Lint Code (push) Successful in -25m16s
Build , Vet Test, and Lint / Build (push) Successful in -25m29s
Tests / Unit Tests (push) Failing after -25m48s
Tests / Integration Tests (push) Failing after -25m53s
2025-12-31 09:41:23 +02:00
Bitech Systems
b99b08430e Merge pull request #16 from bitechdev/fix-funcapi
fix: 🔒 Fixed funcapi possible injections
2025-12-31 09:38:44 +02:00
Hein
fae9a082bd refactor: 🚨 linting issues 2025-12-31 09:37:12 +02:00
Hein Puth (Warkanum)
191822b91c Merge pull request #17 from bitechdev/copilot/sub-pr-16
fix: Comprehensive SQL injection protection in funcspec handlers
2025-12-31 09:35:49 +02:00
copilot-swe-agent[bot]
a6a17d019f fix: Optimized regex performance and added backslash escaping for LIKE patterns
- Added backslash escaping to LIKE pattern sanitization
- Optimized ValidSQL select mode with single pre-compiled regex
- All funcspec tests pass (except pre-existing TestReplaceMetaVariables)

Co-authored-by: warkanum <208308+warkanum@users.noreply.github.com>
2025-12-31 07:28:16 +00:00
copilot-swe-agent[bot]
a7cc42044b fix: Improved SQL injection protection with proper handling
- Fixed IN clause to conditionally quote only string values (not numeric)
- Fixed LIKE pattern sanitization to preserve wildcards while preventing injection
- Improved dangerous pattern removal with case-insensitive regex while preserving case
- All funcspec tests now pass (except pre-existing TestReplaceMetaVariables)

Co-authored-by: warkanum <208308+warkanum@users.noreply.github.com>
2025-12-31 07:24:56 +00:00
copilot-swe-agent[bot]
8cdc353029 fix: Fixed SQL quoting for empty/zero values and sanitized match filter
- Sanitize colval immediately after extraction in match= filter
- Fixed empty/zero value handling to use proper SQL literals (0 vs '')
- Applied proper quoting for string vs numeric comparisons
- Fixed x-fieldfilter handlers for proper value handling

Co-authored-by: warkanum <208308+warkanum@users.noreply.github.com>
2025-12-31 07:22:42 +00:00
copilot-swe-agent[bot]
6528e94297 fix: Improved SQL injection protections based on code review
- Fixed backslash escaping order in colvalue mode
- Added proper quoting for IN clause values
- Simplified dangerous pattern matching with case-insensitive approach
- All funcspec tests pass (except pre-existing test failure)

Co-authored-by: warkanum <208308+warkanum@users.noreply.github.com>
2025-12-31 07:20:56 +00:00
copilot-swe-agent[bot]
f711bf38d2 fix: Enhanced SQL injection protection in funcspec
- Added sanitization for path parameters in mergePathParams
- Added sanitization for query parameters with p- prefix in mergeQueryParams
- Added sanitization for header parameters in mergeHeaderParams
- Fixed IN clause to sanitize all values individually
- Improved ValidSQL function with better escaping and more injection patterns
- Added backslash escaping to colvalue mode
- Extended dangerous keyword list in select mode

Co-authored-by: warkanum <208308+warkanum@users.noreply.github.com>
2025-12-31 07:19:53 +00:00
copilot-swe-agent[bot]
44356d8750 Initial plan 2025-12-31 07:11:57 +00:00
Hein
caf85cf558 fix: 🔒 Fixed funcapi possible injections 2025-12-31 09:09:16 +02:00
2 changed files with 95 additions and 38 deletions

View File

@@ -13,15 +13,23 @@ test-integration:
# Run all tests (unit + integration) # Run all tests (unit + integration)
test: test-unit test-integration test: test-unit test-integration
release-version: ## Create and push a release with specific version (use: make release-version VERSION=v1.2.3) release-version: ## Create and push a release with specific version (use: make release-version VERSION=v1.2.3 or make release-version to auto-increment)
@if [ -z "$(VERSION)" ]; then \ @if [ -z "$(VERSION)" ]; then \
echo "Error: VERSION is required. Usage: make release-version VERSION=v1.2.3"; \ latest_tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"); \
exit 1; \ echo "No VERSION specified. Last version: $$latest_tag"; \
fi version_num=$$(echo "$$latest_tag" | sed 's/^v//'); \
@version="$(VERSION)"; \ major=$$(echo "$$version_num" | cut -d. -f1); \
minor=$$(echo "$$version_num" | cut -d. -f2); \
patch=$$(echo "$$version_num" | cut -d. -f3); \
new_patch=$$((patch + 1)); \
version="v$$major.$$minor.$$new_patch"; \
echo "Auto-incrementing to: $$version"; \
else \
version="$(VERSION)"; \
if ! echo "$$version" | grep -q "^v"; then \ if ! echo "$$version" | grep -q "^v"; then \
version="v$$version"; \ version="v$$version"; \
fi; \ fi; \
fi; \
echo "Creating release: $$version"; \ echo "Creating release: $$version"; \
latest_tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo ""); \ latest_tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo ""); \
if [ -z "$$latest_tag" ]; then \ if [ -z "$$latest_tag" ]; then \

View File

@@ -84,7 +84,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
// Create local copy to avoid modifying the captured parameter across requests // Create local copy to avoid modifying the captured parameter across requests
sqlquery := sqlquery sqlquery := sqlquery
ctx, cancel := context.WithTimeout(r.Context(), 900*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Minute)
defer cancel() defer cancel()
var dbobjlist []map[string]interface{} var dbobjlist []map[string]interface{}
@@ -423,7 +423,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
// Create local copy to avoid modifying the captured parameter across requests // Create local copy to avoid modifying the captured parameter across requests
sqlquery := sqlquery sqlquery := sqlquery
ctx, cancel := context.WithTimeout(r.Context(), 600*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 15*time.Minute)
defer cancel() defer cancel()
propQry := make(map[string]string) propQry := make(map[string]string)
@@ -522,10 +522,17 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
if strings.HasPrefix(kLower, "x-fieldfilter-") { if strings.HasPrefix(kLower, "x-fieldfilter-") {
colname := strings.ReplaceAll(kLower, "x-fieldfilter-", "") colname := strings.ReplaceAll(kLower, "x-fieldfilter-", "")
if strings.Contains(strings.ToLower(sqlquery), colname) { if strings.Contains(strings.ToLower(sqlquery), colname) {
if val == "" || val == "0" { switch val {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("COALESCE(%s, 0) = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue"))) case "0":
} else { sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("COALESCE(%s, 0) = 0", ValidSQL(colname, "colname")))
case "":
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = '' OR %[1]s IS NULL)", ValidSQL(colname, "colname")))
default:
if IsNumeric(val) {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue"))) sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
} else {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = '%s'", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
}
} }
} }
} }
@@ -662,7 +669,10 @@ func (h *Handler) mergePathParams(r *http.Request, sqlquery string, variables ma
for k, v := range pathVars { for k, v := range pathVars {
kword := fmt.Sprintf("[%s]", k) kword := fmt.Sprintf("[%s]", k)
if strings.Contains(sqlquery, kword) { if strings.Contains(sqlquery, kword) {
sqlquery = strings.ReplaceAll(sqlquery, kword, fmt.Sprintf("%v", v)) // Sanitize the value before replacing
vStr := fmt.Sprintf("%v", v)
sanitized := ValidSQL(vStr, "colvalue")
sqlquery = strings.ReplaceAll(sqlquery, kword, sanitized)
} }
variables[k] = v variables[k] = v
@@ -690,7 +700,9 @@ func (h *Handler) mergeQueryParams(r *http.Request, sqlquery string, variables m
// Replace in SQL if placeholder exists // Replace in SQL if placeholder exists
if strings.Contains(sqlquery, kword) && len(val) > 0 { if strings.Contains(sqlquery, kword) && len(val) > 0 {
if strings.HasPrefix(parmk, "p-") { if strings.HasPrefix(parmk, "p-") {
sqlquery = strings.ReplaceAll(sqlquery, kword, val) // Sanitize the parameter value before replacing
sanitized := ValidSQL(val, "colvalue")
sqlquery = strings.ReplaceAll(sqlquery, kword, sanitized)
} }
} }
@@ -702,15 +714,36 @@ func (h *Handler) mergeQueryParams(r *http.Request, sqlquery string, variables m
// Apply filters if allowed // Apply filters if allowed
if allowFilter && len(parmk) > 1 && strings.Contains(strings.ToLower(sqlquery), strings.ToLower(parmk)) { if allowFilter && len(parmk) > 1 && strings.Contains(strings.ToLower(sqlquery), strings.ToLower(parmk)) {
if len(parmv) > 1 { if len(parmv) > 1 {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s IN (%s)", ValidSQL(parmk, "colname"), strings.Join(parmv, ","))) // Sanitize each value in the IN clause with appropriate quoting
sanitizedValues := make([]string, len(parmv))
for i, v := range parmv {
if IsNumeric(v) {
// Numeric values don't need quotes
sanitizedValues[i] = ValidSQL(v, "colvalue")
} else {
// String values need quotes
sanitized := ValidSQL(v, "colvalue")
sanitizedValues[i] = fmt.Sprintf("'%s'", sanitized)
}
}
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s IN (%s)", ValidSQL(parmk, "colname"), strings.Join(sanitizedValues, ",")))
} else { } else {
if strings.Contains(val, "match=") { if strings.Contains(val, "match=") {
colval := strings.ReplaceAll(val, "match=", "") colval := strings.ReplaceAll(val, "match=", "")
// Escape single quotes and backslashes for LIKE patterns
// But don't escape wildcards % and _ which are intentional
colval = strings.ReplaceAll(colval, "\\", "\\\\")
colval = strings.ReplaceAll(colval, "'", "''")
if colval != "*" { if colval != "*" {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s ILIKE '%%%s%%'", ValidSQL(parmk, "colname"), ValidSQL(colval, "colvalue"))) sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s ILIKE '%%%s%%'", ValidSQL(parmk, "colname"), colval))
} }
} else if val == "" || val == "0" { } else if val == "" || val == "0" {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = %[2]s OR %[1]s IS NULL)", ValidSQL(parmk, "colname"), ValidSQL(val, "colvalue"))) // For empty/zero values, treat as literal 0 or empty string with quotes
if val == "0" {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = 0 OR %[1]s IS NULL)", ValidSQL(parmk, "colname")))
} else {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = '' OR %[1]s IS NULL)", ValidSQL(parmk, "colname")))
}
} else { } else {
if IsNumeric(val) { if IsNumeric(val) {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(parmk, "colname"), ValidSQL(val, "colvalue"))) sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(parmk, "colname"), ValidSQL(val, "colvalue")))
@@ -743,16 +776,25 @@ func (h *Handler) mergeHeaderParams(r *http.Request, sqlquery string, variables
kword := fmt.Sprintf("[%s]", k) kword := fmt.Sprintf("[%s]", k)
if strings.Contains(sqlquery, kword) { if strings.Contains(sqlquery, kword) {
sqlquery = strings.ReplaceAll(sqlquery, kword, val) // Sanitize the header value before replacing
sanitized := ValidSQL(val, "colvalue")
sqlquery = strings.ReplaceAll(sqlquery, kword, sanitized)
} }
// Handle special headers // Handle special headers
if strings.Contains(k, "x-fieldfilter-") { if strings.Contains(k, "x-fieldfilter-") {
colname := strings.ReplaceAll(k, "x-fieldfilter-", "") colname := strings.ReplaceAll(k, "x-fieldfilter-", "")
if val == "" || val == "0" { switch val {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("COALESCE(%s, 0) = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue"))) case "0":
} else { sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("COALESCE(%s, 0) = 0", ValidSQL(colname, "colname")))
case "":
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("(%[1]s = '' OR %[1]s IS NULL)", ValidSQL(colname, "colname")))
default:
if IsNumeric(val) {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue"))) sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = %s", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
} else {
sqlquery = sqlQryWhere(sqlquery, fmt.Sprintf("%s = '%s'", ValidSQL(colname, "colname"), ValidSQL(val, "colvalue")))
}
} }
} }
@@ -782,12 +824,15 @@ func (h *Handler) mergeHeaderParams(r *http.Request, sqlquery string, variables
func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx *security.UserContext, metainfo map[string]interface{}, variables map[string]interface{}) string { func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx *security.UserContext, metainfo map[string]interface{}, variables map[string]interface{}) string {
if strings.Contains(sqlquery, "[p_meta_default]") { if strings.Contains(sqlquery, "[p_meta_default]") {
data, _ := json.Marshal(metainfo) data, _ := json.Marshal(metainfo)
sqlquery = strings.ReplaceAll(sqlquery, "[p_meta_default]", fmt.Sprintf("'%s'::jsonb", string(data))) dataStr := strings.ReplaceAll(string(data), "$META$", "/*META*/")
sqlquery = strings.ReplaceAll(sqlquery, "[p_meta_default]", fmt.Sprintf("$META$%s$META$::jsonb", dataStr))
} }
if strings.Contains(sqlquery, "[json_variables]") { if strings.Contains(sqlquery, "[json_variables]") {
data, _ := json.Marshal(variables) data, _ := json.Marshal(variables)
sqlquery = strings.ReplaceAll(sqlquery, "[json_variables]", fmt.Sprintf("'%s'::jsonb", string(data))) dataStr := strings.ReplaceAll(string(data), "$VAR$", "/*VAR*/")
sqlquery = strings.ReplaceAll(sqlquery, "[json_variables]", fmt.Sprintf("$VAR$%s$VAR$::jsonb", dataStr))
} }
if strings.Contains(sqlquery, "[rid_user]") { if strings.Contains(sqlquery, "[rid_user]") {
@@ -795,7 +840,7 @@ func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx
} }
if strings.Contains(sqlquery, "[user]") { if strings.Contains(sqlquery, "[user]") {
sqlquery = strings.ReplaceAll(sqlquery, "[user]", fmt.Sprintf("'%s'", userCtx.UserName)) sqlquery = strings.ReplaceAll(sqlquery, "[user]", fmt.Sprintf("$USR$%s$USR$", strings.ReplaceAll(userCtx.UserName, "$USR$", "/*USR*/")))
} }
if strings.Contains(sqlquery, "[rid_session]") { if strings.Contains(sqlquery, "[rid_session]") {
@@ -806,7 +851,7 @@ func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx
} }
if strings.Contains(sqlquery, "[method]") { if strings.Contains(sqlquery, "[method]") {
sqlquery = strings.ReplaceAll(sqlquery, "[method]", r.Method) sqlquery = strings.ReplaceAll(sqlquery, "[method]", fmt.Sprintf("$M$%s$M$", strings.ReplaceAll(r.Method, "$M$", "/*M*/")))
} }
if strings.Contains(sqlquery, "[post_body]") { if strings.Contains(sqlquery, "[post_body]") {
@@ -819,7 +864,7 @@ func (h *Handler) replaceMetaVariables(sqlquery string, r *http.Request, userCtx
} }
} }
} }
sqlquery = strings.ReplaceAll(sqlquery, "[post_body]", fmt.Sprintf("'%s'", bodystr)) sqlquery = strings.ReplaceAll(sqlquery, "[post_body]", fmt.Sprintf("$PBODY$%s$PBODY$", strings.ReplaceAll(bodystr, "$PBODY$", "/*PBODY*/")))
} }
return sqlquery return sqlquery
@@ -859,19 +904,23 @@ func ValidSQL(input, mode string) string {
reg := regexp.MustCompile(`[^a-zA-Z0-9_\.]`) reg := regexp.MustCompile(`[^a-zA-Z0-9_\.]`)
return reg.ReplaceAllString(input, "") return reg.ReplaceAllString(input, "")
case "colvalue": case "colvalue":
// For column values, escape single quotes // For column values, escape single quotes and backslashes
return strings.ReplaceAll(input, "'", "''") // Note: Backslashes must be escaped first, then single quotes
result := strings.ReplaceAll(input, "\\", "\\\\")
result = strings.ReplaceAll(result, "'", "''")
return result
case "select": case "select":
// For SELECT clauses, be more permissive but still safe // For SELECT clauses, be more permissive but still safe
// Remove semicolons and common SQL injection patterns // Remove semicolons and common SQL injection patterns (case-insensitive)
dangerous := []string{";", "--", "/*", "*/", "xp_", "sp_", "DROP ", "DELETE ", "TRUNCATE ", "UPDATE ", "INSERT "} dangerous := []string{
result := input ";", "--", "/\\*", "\\*/", "xp_", "sp_",
for _, d := range dangerous { "drop ", "delete ", "truncate ", "update ", "insert ",
result = strings.ReplaceAll(result, d, "") "exec ", "execute ", "union ", "declare ", "alter ", "create ",
result = strings.ReplaceAll(result, strings.ToLower(d), "")
result = strings.ReplaceAll(result, strings.ToUpper(d), "")
} }
return result // Build a single regex pattern with all dangerous keywords
pattern := "(?i)(" + strings.Join(dangerous, "|") + ")"
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(input, "")
default: default:
return input return input
} }