Module: Parse::Agent::ConstraintTranslator
- Extended by:
- ConstraintTranslator
- Included in:
- ConstraintTranslator
- Defined in:
- lib/parse/agent/constraint_translator.rb
Overview
The ConstraintTranslator converts JSON-style query constraints (like those from LLM function calls) into Parse REST API format.
It enforces strict security validation:
-
Blocks dangerous operators that allow code execution ($where, $function, etc.)
-
Rejects unknown operators (whitelist-based approach)
-
Limits query depth to prevent DoS attacks
Defined Under Namespace
Classes: ConstraintSecurityError, InvalidOperatorError
Constant Summary collapse
- BLOCKED_OPERATORS =
Operators that are BLOCKED - they allow arbitrary code execution These are blocked regardless of permission level
%w[ $where $function $accumulator $expr ].freeze
- ALLOWED_OPERATORS =
Whitelist of allowed Parse query operators
%w[ $lt $lte $gt $gte $ne $eq $in $nin $all $exists $regex $options $text $search $near $nearSphere $geoWithin $geoIntersects $centerSphere $box $polygon $geometry $maxDistance $maxDistanceInMiles $maxDistanceInKilometers $maxDistanceInRadians $relatedTo $inQuery $notInQuery $containedIn $containsAll $select $dontSelect $or $and $nor ].freeze
- CROSS_CLASS_OPERATORS =
Operators whose value carries an inner sub-query of the shape {className:, where:, key:}. Each must be validated through Tools.assert_class_accessible! so the LLM cannot reach into a hidden class via the sub-query, and the inner
wheremust be recursively re-translated so blocked operators inside it are also caught. %w[ $inQuery $notInQuery $select $dontSelect ].freeze
- DENIED_WHERE_KEYS =
Field-name keys (non-operator) that are never permitted in a caller-supplied where: constraint, regardless of class or permission level. These are internal Parse Server columns whose presence in a $match filter creates a 1-bit-per-query oracle that can exfiltrate bcrypt hashes, session tokens, or reset tokens character-by-character via count deltas. The list covers:
-
Exact names (lowercased storage form and camelCase API form)
-
A prefix that catches per-provider columns stored as ‘_auth_data_facebook`, `_auth_data_google`, etc.
Mirrored in Parse::PipelineSecurity::INTERNAL_FIELDS_DENYLIST so the aggregate pipeline path is covered independently (the two modules can be loaded in any order; duplication is intentional).
-
%w[ _hashed_password _password_history _session_token _sessionToken _email_verify_token _perishable_token _failed_login_count _account_lockout_expires_at _rperm _wperm _auth_data ].freeze
- DENIED_WHERE_KEY_PREFIXES =
Prefix-based check (catches _auth_data_facebook, _auth_data_google, …).
%w[_auth_data_].freeze
- MAX_QUERY_DEPTH =
Maximum query depth to prevent DoS via deeply nested structures
8- MAX_REGEX_PATTERN_LENGTH =
NEW-TOOLS-7: cap $regex pattern length. Patterns larger than this are rejected before reaching MongoDB. 256 is generous for the legitimate analyst-facing patterns the agent surface is designed for (prefix anchors, simple character classes) while keeping the worst-case backtracking cost on any one pattern bounded.
256- ALLOWED_REGEX_OPTIONS =
Allowed $options flag characters. MongoDB accepts i (case insensitive), m (multi-line), x (extended/whitespace-ignored), s (dot-all). The dot-all ‘s` flag is intentionally omitted: it makes `.` cross newlines, which extends the search frontier on multi-line text fields and amplifies catastrophic-backtracking cost for the worst patterns. `imx` covers every real use case the agent surface needs.
"imx"- REDOS_NESTED_QUANTIFIER_RE =
Heuristic for nested-quantifier ReDoS patterns (catastrophic backtracking). Matches a quantifier (‘+` or `*`) INSIDE a parenthesized group that is itself followed by a quantifier (`+`, `*`, or `?`) — the structural shape that drives exponential time on adversarial inputs (`(a+)+`, `(a*)*`, `(x|y)+?` are all reachable). Stricter than the audit’s suggested heuristic, which would false-positive on innocuous patterns like ‘^foo.bar.$`. Anchored prefixes without nested-quantifier-groups (`^bar(a+)+` is still refused; plain `^foo.*` is not).
/\([^)]*[+*][^)]*\)[+*?]/.freeze
Instance Method Summary collapse
-
#translate(constraints, agent = nil) ⇒ Hash
Translate JSON constraints to Parse query format.
-
#valid?(constraints) ⇒ Boolean
Check if constraints are valid without raising.
Instance Method Details
#translate(constraints, agent = nil) ⇒ Hash
Translate JSON constraints to Parse query format. Validates all operators against the security whitelist.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/parse/agent/constraint_translator.rb', line 156 def translate(constraints, agent = nil) return {} if constraints.nil? || constraints.empty? raise InvalidOperatorError.new( "Constraints must be a Hash, got #{constraints.class}", operator: nil, ) unless constraints.is_a?(Hash) constraints.transform_keys(&:to_s).each_with_object({}) do |(key, value), result| # Check for blocked operators at the root level if key.start_with?("$") validate_operator!(key) end # H1 / M1: reject keys that reference internal Parse Server columns. # These enable bcrypt-hash and session-token oracle attacks via # count deltas even when operators are otherwise clean. assert_where_key_permitted!(key) result[columnize(key)] = translate_value(value, depth: 0, agent: agent) end end |
#valid?(constraints) ⇒ Boolean
Check if constraints are valid without raising.
181 182 183 184 185 186 |
# File 'lib/parse/agent/constraint_translator.rb', line 181 def valid?(constraints) translate(constraints) true rescue ConstraintSecurityError, InvalidOperatorError false end |