
Research
Node.js Fixes AsyncLocalStorage Crash Bug That Could Take Down Production Servers
Node.js patched a crash bug where AsyncLocalStorage could cause stack overflows to bypass error handlers and terminate production servers.
A Go package that converts Common Expression Language (CEL) expressions to SQL WHERE clauses using the Squirrel SQL builder.
cel2squirrel enables filtering using CEL expressions (as specified in AIP-160) and converts them to SQL queries that can be executed against a database. It validates that expressions are boolean and provides type-safe conversion to Squirrel SQL builder objects.
==, !=, <, <=, >, >=&& (AND), || (OR), ! (NOT)contains(), startsWith(), endsWith()in operator== null, != null$1, $2, etc.)go get zntr.io/cel2squirrel
package main
import (
"fmt"
"log"
"github.com/Masterminds/squirrel"
"github.com/google/cel-go/cel"
"zntr.io/cel2squirrel"
)
func main() {
// Define field declarations (CEL variables and their types)
config := cel2squirrel.Config{
FieldDeclarations: map[string]*cel.Type{
"status": cel.StringType,
"age": cel.IntType,
"rating": cel.DoubleType,
},
}
// Create converter
converter, err := cel2squirrel.NewConverter(config)
if err != nil {
log.Fatal(err)
}
// Convert CEL expression to SQL
celExpr := `status == "published" && age >= 18`
result, err := converter.Convert(celExpr, nil)
if err != nil {
log.Fatal(err)
}
// Build complete query with Squirrel
query := squirrel.Select("*").
From("users").
Where(result.Where)
sql, args, _ := query.ToSql()
fmt.Println(sql)
// Output: SELECT * FROM users WHERE (status = ? AND age >= ?)
fmt.Println(args)
// Output: [published 18]
}
Map CEL field names to different SQL column names:
config := cel2squirrel.Config{
FieldDeclarations: map[string]*cel.Type{
"isDraft": cel.BoolType,
"ownerId": cel.StringType,
},
}
converter, _ := cel2squirrel.NewConverter(config)
// Map camelCase CEL fields to snake_case SQL columns
fieldMappings := map[string]string{
"isDraft": "is_draft",
"ownerId": "owner_id",
}
celExpr := `isDraft == false && ownerId == "user123"`
result, _ := converter.Convert(celExpr, fieldMappings)
sql, args, _ := result.Where.ToSql()
// SQL: (is_draft = ? AND owner_id = ?)
// Args: [false user123]
Use PostgreSQL-style numbered placeholders:
query := squirrel.Select("*").
From("users").
Where(result.Where).
PlaceholderFormat(squirrel.Dollar)
sql, args, _ := query.ToSql()
// SQL: SELECT * FROM users WHERE (status = $1 AND age >= $2)
Handle complex nested boolean expressions:
celExpr := `(status == "published" || status == "featured") && age >= 18 && rating > 4.0`
result, _ := converter.Convert(celExpr, nil)
query := squirrel.Select("id", "name", "rating").
From("users").
Where(result.Where).
OrderBy("rating DESC").
Limit(10)
sql, args, _ := query.ToSql()
// SQL: SELECT id, name, rating FROM users
// WHERE ((status = ? OR status = ?) AND age >= ? AND rating > ?)
// ORDER BY rating DESC LIMIT 10
// Args: [published featured 18 4.0]
Use CEL string methods:
// Contains
celExpr := `name.contains("john")`
// SQL: name LIKE ?
// Args: [%john%]
// Starts with
celExpr := `name.startsWith("Dr. ")`
// SQL: name LIKE ?
// Args: [Dr. %]
// Ends with
celExpr := `email.endsWith("@example.com")`
// SQL: email LIKE ?
// Args: [%@example.com]
Handle NULL values:
// IS NULL
celExpr := `deletedAt == null`
// SQL: deletedAt IS NULL
// IS NOT NULL
celExpr := `deletedAt != null`
// SQL: deletedAt IS NOT NULL
Filter with multiple values:
celExpr := `status in ["published", "featured", "archived"]`
result, _ := converter.Convert(celExpr, nil)
sql, args, _ := result.Where.ToSql()
// SQL: status IN (?,?,?)
// Args: [published featured archived]
Example implementation of a database repository with CEL filtering (AIP-160 compliant):
package repository
import (
"context"
"fmt"
"github.com/Masterminds/squirrel"
"github.com/google/cel-go/cel"
"zntr.io/cel2squirrel"
)
type UserRepository struct {
db *sql.DB
converter *cel2squirrel.Converter
}
func NewUserRepository(db *sql.DB) (*UserRepository, error) {
// Setup CEL converter
config := cel2squirrel.Config{
FieldDeclarations: map[string]*cel.Type{
"status": cel.StringType,
"isActive": cel.BoolType,
"rating": cel.DoubleType,
"createTime": cel.TimestampType,
},
}
converter, err := cel2squirrel.NewConverter(config)
if err != nil {
return nil, fmt.Errorf("failed to create CEL converter: %w", err)
}
return &UserRepository{
db: db,
converter: converter,
}, nil
}
func (r *UserRepository) Search(ctx context.Context, filter string, pageSize int) ([]User, error) {
// Map CEL fields to SQL columns
fieldMappings := map[string]string{
"isActive": "is_active",
"createTime": "create_time",
}
// Convert CEL filter to SQL
result, err := r.converter.Convert(filter, fieldMappings)
if err != nil {
return nil, fmt.Errorf("invalid filter expression: %w", err)
}
// Build query with Squirrel
query := squirrel.Select("*").
From("users").
Where(result.Where).
PlaceholderFormat(squirrel.Dollar).
Limit(uint64(pageSize))
sql, args, err := query.ToSql()
if err != nil {
return nil, err
}
// Execute query
rows, err := r.db.QueryContext(ctx, sql, args...)
if err != nil {
return nil, err
}
defer rows.Close()
// Scan results...
var users []User
for rows.Next() {
var u User
// Scan into user struct...
users = append(users, u)
}
return users, rows.Err()
}
GET /v1/users?filter=status == "active" && rating > 4.0&page_size=20
| CEL Operator | SQL Equivalent | Example |
|---|---|---|
== | = | status == "published" |
!= | <> or IS NOT | status != "draft" |
< | < | age < 18 |
<= | <= | age <= 21 |
> | > | age > 65 |
>= | >= | rating >= 4.5 |
| CEL Operator | SQL Equivalent | Example |
|---|---|---|
&& | AND | status == "published" && age >= 18 |
|| | OR | status == "draft" || status == "published" |
! | NOT | !(isDraft) |
| CEL Function | SQL Equivalent | Example |
|---|---|---|
contains(x) | LIKE '%x%' | label.contains("test") |
startsWith(x) | LIKE 'x%' | label.startsWith("prod") |
endsWith(x) | LIKE '%x' | label.endsWith("v2") |
| CEL Operator | SQL Equivalent | Example |
|---|---|---|
in | IN (...) | status in ["published", "featured"] |
| CEL Expression | SQL Equivalent | Example |
|---|---|---|
field == null | IS NULL | deletedAt == null |
field != null | IS NOT NULL | deletedAt != null |
The converter validates expressions and returns descriptive errors:
// Non-boolean expression
celExpr := `age + 5` // Returns: "CEL expression must return a boolean, got int"
// Undefined field
celExpr := `unknownField == "value"` // Returns: "failed to compile CEL expression: undeclared reference..."
// Syntax error
celExpr := `status == ` // Returns: "failed to compile CEL expression: Syntax error..."
Use CEL types directly to define field types:
import "github.com/google/cel-go/cel"
FieldDeclarations: map[string]*cel.Type{
"stringField": cel.StringType,
"intField": cel.IntType,
"doubleField": cel.DoubleType,
"boolField": cel.BoolType,
"uintField": cel.UintType,
"timestampField": cel.TimestampType,
"durationField": cel.DurationType,
"bytesField": cel.BytesType,
// Complex types
"stringList": cel.ListType(cel.StringType),
"stringMap": cel.MapType(cel.StringType, cel.StringType),
}
in operator requires constant list literalsage + 5 > 30 are not supportedThe cel2squirrel package implements multiple layers of security to protect against common vulnerabilities when converting user-supplied filter expressions to SQL queries. All security features are enabled by default with secure defaults.
LIKE Pattern Escaping: The package automatically escapes SQL special characters (%, _, \, [, ]) in string operations to prevent LIKE pattern injection:
// User input: name.contains("%")
// Unsafe: LIKE '%%%' (matches all records!)
// Safe: LIKE '%\%%' (matches literal % character)
celExpr := `name.contains("%admin")`
result, _ := converter.Convert(celExpr)
// Generates: name LIKE ?
// Args: [%\%admin%] // Special chars properly escaped
Parameterized Queries: All values are passed as query parameters, never concatenated into SQL strings:
celExpr := `status == "published" && age > 18`
// Generates parameterized SQL: status = ? AND age > ?
// Args: ["published", 18]
Configure expression complexity limits to prevent resource exhaustion:
config := cel2squirrel.Config{
FieldDeclarations: map[string]cel2squirrel.ColumnMapping{
"status": {Type: cel.StringType, Column: "status"},
},
// Security limits (default values shown)
MaxExpressionLength: 10000, // Max 10KB expression
MaxExpressionDepth: 50, // Max 50 levels of nesting
MaxInClauseSize: 1000, // Max 1000 values in IN clause
}
converter, _ := cel2squirrel.NewConverter(config)
// These will be rejected:
_, err := converter.Convert(strings.Repeat("a", 20000)) // Too long
_, err := converter.Convert(deeplyNestedExpression) // Too deep
_, err := converter.Convert(`status in [...]`) // Too many values
Restrict which fields users can filter by based on their roles:
config := cel2squirrel.Config{
FieldDeclarations: map[string]cel2squirrel.ColumnMapping{
"status": {Type: cel.StringType, Column: "status"},
"owner_id": {Type: cel.StringType, Column: "owner_id"},
"salary": {Type: cel.IntType, Column: "salary"},
},
// Public fields accessible to all users
PublicFields: []string{"status"},
// Role-based access control
FieldACL: map[string][]string{
"owner_id": {"admin", "manager"},
"salary": {"admin", "hr"},
},
}
converter, _ := cel2squirrel.NewConverter(config)
// Check authorization before conversion
userRoles := []string{"user"}
_, err := converter.ConvertWithAuth(`owner_id == "user123"`, userRoles)
// Returns: "access denied: insufficient permissions"
adminRoles := []string{"admin"}
result, _ := converter.ConvertWithAuth(`owner_id == "user123"`, adminRoles)
// Success: admin can filter by owner_id
The package sanitizes error messages to prevent information disclosure:
// Internal error with details (logged server-side)
convErr := err.(*cel2squirrel.ConversionError)
log.Printf("Conversion failed: %v", convErr.InternalError)
// Logs: "undeclared reference to 'secretField'"
// Public error message (returned to user)
fmt.Println(convErr.Error())
// Returns: "invalid filter expression syntax"
// Does NOT reveal field names or internal structure
Defense-in-depth type checking validates that values match declared field types:
config := cel2squirrel.Config{
FieldDeclarations: map[string]cel2squirrel.ColumnMapping{
"age": {Type: cel.IntType, Column: "age"},
},
}
converter, _ := cel2squirrel.NewConverter(config)
// CEL's type system catches this at compile time
_, err := converter.Convert(`age == "not a number"`)
// Returns type mismatch error
// Runtime validation provides additional protection
A production-ready secure configuration:
func NewSecureConverter() (*cel2squirrel.Converter, error) {
config := cel2squirrel.DefaultConfig() // Start with secure defaults
config.FieldDeclarations = map[string]cel2squirrel.ColumnMapping{
"status": {Type: cel.StringType, Column: "status"},
"created_at": {Type: cel.TimestampType, Column: "created_at"},
"owner_id": {Type: cel.StringType, Column: "owner_id"},
"is_private": {Type: cel.BoolType, Column: "is_private"},
}
// Define public fields
config.PublicFields = []string{"status", "created_at"}
// Restrict sensitive fields
config.FieldACL = map[string][]string{
"owner_id": {"admin", "manager"},
"is_private": {"admin"},
}
// Adjust limits for your use case
config.MaxExpressionLength = 1000 // Shorter limit for API
config.MaxExpressionDepth = 10 // Prevent deeply nested attacks
config.MaxInClauseSize = 100 // Reasonable batch size
return cel2squirrel.NewConverter(config)
}
// Usage in HTTP handler
func (h *Handler) SearchRecords(w http.ResponseWriter, r *http.Request) {
filter := r.URL.Query().Get("filter")
// Get user roles from authentication context
userRoles := h.getUserRoles(r.Context())
// Convert with authorization check
result, err := h.converter.ConvertWithAuth(filter, userRoles)
if err != nil {
if convErr, ok := err.(*cel2squirrel.ConversionError); ok {
// Return sanitized error to user
http.Error(w, convErr.PublicMessage, http.StatusBadRequest)
// Log detailed error server-side
log.Printf("Filter conversion failed: %v (code=%s)",
convErr.InternalError, convErr.ErrorCode)
return
}
http.Error(w, "Invalid filter", http.StatusBadRequest)
return
}
// Build and execute query...
}
Run tests:
go test -v ./...
Run with coverage:
go test -cover ./...
Run fuzz tests:
go test -fuzz=Fuzz -fuzztime=30s
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
go mod downloadgo test ./...go vet ./...This library is ideal for:
MIT License - see LICENSE file for details.
Built with:
FAQs
Unknown package
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Node.js patched a crash bug where AsyncLocalStorage could cause stack overflows to bypass error handlers and terminate production servers.

Research
/Security News
A malicious Chrome extension steals newly created MEXC API keys, exfiltrates them to Telegram, and enables full account takeover with trading and withdrawal rights.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.