Skip to content

Commit cfb6d81

Browse files
fix: Add SQL output length limits to prevent DoS (fixes #33) (#69)
This commit addresses issue #33 by implementing SQL output length limits to prevent Denial of Service attacks through resource exhaustion. ## Changes ### Core Implementation (cel2sql.go) - Added `defaultMaxSQLOutputLength = 50000` constant - Added `maxOutputLen` field to `convertOptions` and `converter` structs - Created `WithMaxOutputLength()` functional option for custom limits - Implemented output length check in `visit()` method - Both `Convert()` and `ConvertParameterized()` now respect the limit ### Tests (output_length_test.go) - Comprehensive test coverage for all scenarios: - Default and custom output length limits - Combination with other options (context, schemas, logger, maxDepth) - Error message validation - Counter reset between calls - Large arrays, string concatenations, comprehensions - Parameterized query support ### Documentation - Updated CLAUDE.md with new "Resource Exhaustion Protection" section - Updated README.md security features to include SQL output length limits - Added examples for using `WithMaxOutputLength()` ## Security Impact - Prevents DoS attacks via extremely large SQL output - Addresses CWE-400 (Uncontrolled Resource Consumption) - Default limit: 50,000 characters (configurable) - Works seamlessly with existing security features ## Testing - All tests pass (make test) - Code passes linting (make lint) - Coverage maintained at 90%+ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 40f34b0 commit cfb6d81

File tree

4 files changed

+668
-19
lines changed

4 files changed

+668
-19
lines changed

CLAUDE.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,50 @@ field.matches(r"(((((((((((a")) // Excessive nesting
386386
- CPU exhaustion from complex patterns
387387
- Service disruption from malicious regex
388388

389+
#### Resource Exhaustion Protection
390+
391+
cel2sql includes multiple layers of protection against resource exhaustion attacks (CWE-400):
392+
393+
**Recursion Depth Limits:**
394+
- **Default limit**: 100 levels of expression nesting
395+
- **Configurable**: Use `WithMaxDepth()` to adjust
396+
- **Protection**: Prevents stack overflow from deeply nested expressions (CWE-674)
397+
398+
**SQL Output Length Limits:**
399+
- **Default limit**: 50,000 characters of generated SQL
400+
- **Configurable**: Use `WithMaxOutputLength()` to adjust
401+
- **Protection**: Prevents memory exhaustion from extremely large SQL queries
402+
403+
**Comprehension Depth Limits:**
404+
- **Fixed limit**: 3 levels of nested comprehensions
405+
- **Protection**: Prevents resource exhaustion from deeply nested UNNEST/subquery operations
406+
407+
**Examples:**
408+
```go
409+
// Use default limits (recommended)
410+
sql, err := cel2sql.Convert(ast)
411+
412+
// Custom recursion depth limit
413+
sql, err := cel2sql.Convert(ast,
414+
cel2sql.WithMaxDepth(150))
415+
416+
// Custom SQL output length limit
417+
sql, err := cel2sql.Convert(ast,
418+
cel2sql.WithMaxOutputLength(100000))
419+
420+
// Combine multiple limits
421+
sql, err := cel2sql.Convert(ast,
422+
cel2sql.WithMaxDepth(75),
423+
cel2sql.WithMaxOutputLength(25000),
424+
cel2sql.WithContext(ctx))
425+
```
426+
427+
**Protection Against:**
428+
- Stack overflow from deeply nested expressions
429+
- Memory exhaustion from extremely large SQL output
430+
- CPU/memory exhaustion from deeply nested comprehensions
431+
- DoS attacks via resource consumption
432+
389433
#### Context Timeouts
390434

391435
Use context timeouts as defense-in-depth:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ cel2sql includes comprehensive security protections:
7575
- 🔒 **JSON Field Escaping** - Automatic quote escaping in JSON paths
7676
- 🚫 **ReDoS Protection** - Validates regex patterns to prevent catastrophic backtracking
7777
- 🔄 **Recursion Depth Limits** - Prevents stack overflow from deeply nested expressions (default: 100)
78+
- 📏 **SQL Output Length Limits** - Prevents memory exhaustion from extremely large SQL queries (default: 50,000 chars)
7879
- ⏱️ **Context Timeouts** - Optional timeout protection for complex expressions
7980

8081
All security features are enabled by default with zero configuration required.

cel2sql.go

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,22 @@ const (
4343
// maxComprehensionDepth is the maximum nesting depth for CEL comprehensions
4444
// to prevent resource exhaustion from deeply nested UNNEST/subquery operations (CWE-400).
4545
maxComprehensionDepth = 3
46+
47+
// defaultMaxSQLOutputLength is the default maximum length of generated SQL output
48+
// to prevent resource exhaustion from extremely large SQL queries (CWE-400).
49+
defaultMaxSQLOutputLength = 50000
4650
)
4751

4852
// ConvertOption is a functional option for configuring the Convert function.
4953
type ConvertOption func(*convertOptions)
5054

5155
// convertOptions holds configuration options for the Convert function.
5256
type convertOptions struct {
53-
schemas map[string]pg.Schema
54-
ctx context.Context
55-
logger *slog.Logger
56-
maxDepth int // Maximum recursion depth (0 = use default)
57+
schemas map[string]pg.Schema
58+
ctx context.Context
59+
logger *slog.Logger
60+
maxDepth int // Maximum recursion depth (0 = use default)
61+
maxOutputLen int // Maximum SQL output length (0 = use default)
5762
}
5863

5964
// WithSchemas provides schema information for proper JSON/JSONB field handling.
@@ -137,6 +142,26 @@ func WithMaxDepth(maxDepth int) ConvertOption {
137142
}
138143
}
139144

145+
// WithMaxOutputLength sets the maximum length of generated SQL output.
146+
// If not provided, defaultMaxSQLOutputLength (50000) is used.
147+
// This protects against resource exhaustion from extremely large SQL queries (CWE-400).
148+
//
149+
// Example with custom output length limit:
150+
//
151+
// sql, err := cel2sql.Convert(ast, cel2sql.WithMaxOutputLength(100000))
152+
//
153+
// Example with multiple options:
154+
//
155+
// sql, err := cel2sql.Convert(ast,
156+
// cel2sql.WithMaxOutputLength(25000),
157+
// cel2sql.WithMaxDepth(50),
158+
// cel2sql.WithContext(ctx))
159+
func WithMaxOutputLength(maxLength int) ConvertOption {
160+
return func(o *convertOptions) {
161+
o.maxOutputLen = maxLength
162+
}
163+
}
164+
140165
// Result represents the output of a CEL to SQL conversion with parameterized queries.
141166
// It contains the SQL string with placeholders ($1, $2, etc.) and the corresponding parameter values.
142167
type Result struct {
@@ -158,8 +183,9 @@ func Convert(ast *cel.Ast, opts ...ConvertOption) (string, error) {
158183
start := time.Now()
159184

160185
options := &convertOptions{
161-
logger: slog.New(slog.DiscardHandler), // Default: no-op logger with zero overhead
162-
maxDepth: defaultMaxRecursionDepth, // Default: 100 recursion depth limit
186+
logger: slog.New(slog.DiscardHandler), // Default: no-op logger with zero overhead
187+
maxDepth: defaultMaxRecursionDepth, // Default: 100 recursion depth limit
188+
maxOutputLen: defaultMaxSQLOutputLength, // Default: 50000 character output limit
163189
}
164190
for _, opt := range opts {
165191
opt(options)
@@ -174,11 +200,12 @@ func Convert(ast *cel.Ast, opts ...ConvertOption) (string, error) {
174200
}
175201

176202
un := &converter{
177-
typeMap: checkedExpr.TypeMap,
178-
schemas: options.schemas,
179-
ctx: options.ctx,
180-
logger: options.logger,
181-
maxDepth: options.maxDepth,
203+
typeMap: checkedExpr.TypeMap,
204+
schemas: options.schemas,
205+
ctx: options.ctx,
206+
logger: options.logger,
207+
maxDepth: options.maxDepth,
208+
maxOutputLen: options.maxOutputLen,
182209
}
183210

184211
if err := un.visit(checkedExpr.Expr); err != nil {
@@ -225,8 +252,9 @@ func ConvertParameterized(ast *cel.Ast, opts ...ConvertOption) (*Result, error)
225252
start := time.Now()
226253

227254
options := &convertOptions{
228-
logger: slog.New(slog.DiscardHandler), // Default: no-op logger with zero overhead
229-
maxDepth: defaultMaxRecursionDepth, // Default: 100 recursion depth limit
255+
logger: slog.New(slog.DiscardHandler), // Default: no-op logger with zero overhead
256+
maxDepth: defaultMaxRecursionDepth, // Default: 100 recursion depth limit
257+
maxOutputLen: defaultMaxSQLOutputLength, // Default: 50000 character output limit
230258
}
231259
for _, opt := range opts {
232260
opt(options)
@@ -241,12 +269,13 @@ func ConvertParameterized(ast *cel.Ast, opts ...ConvertOption) (*Result, error)
241269
}
242270

243271
un := &converter{
244-
typeMap: checkedExpr.TypeMap,
245-
schemas: options.schemas,
246-
ctx: options.ctx,
247-
logger: options.logger,
248-
maxDepth: options.maxDepth,
249-
parameterize: true, // Enable parameterization
272+
typeMap: checkedExpr.TypeMap,
273+
schemas: options.schemas,
274+
ctx: options.ctx,
275+
logger: options.logger,
276+
maxDepth: options.maxDepth,
277+
maxOutputLen: options.maxOutputLen,
278+
parameterize: true, // Enable parameterization
250279
}
251280

252281
if err := un.visit(checkedExpr.Expr); err != nil {
@@ -278,6 +307,7 @@ type converter struct {
278307
logger *slog.Logger
279308
depth int // Current recursion depth
280309
maxDepth int // Maximum allowed recursion depth
310+
maxOutputLen int // Maximum allowed SQL output length
281311
comprehensionDepth int // Current comprehension nesting depth
282312
parameterize bool // Enable parameterized output
283313
parameters []interface{} // Collected parameters for parameterized queries
@@ -313,6 +343,11 @@ func (con *converter) visit(expr *exprpb.Expr) error {
313343
return err
314344
}
315345

346+
// Check SQL output length limit to prevent resource exhaustion (CWE-400)
347+
if con.str.Len() > con.maxOutputLen {
348+
return fmt.Errorf("generated SQL exceeds maximum output length of %d", con.maxOutputLen)
349+
}
350+
316351
switch expr.ExprKind.(type) {
317352
case *exprpb.Expr_CallExpr:
318353
return con.visitCall(expr)

0 commit comments

Comments
 (0)