Module: Inquirex::SafeSource

Defined in:
lib/inquirex/safe_source.rb,
lib/inquirex/safe_source/call_spec.rb,
lib/inquirex/safe_source/validator.rb,
lib/inquirex/safe_source/vocabulary.rb

Overview

Decides whether a string of flow DSL is safe to evaluate — without evaluating it.

load_dsl is an eval. Whenever the text comes from somewhere other than your own repository — a database column a customer edits, an upload, an LLM, a visual builder's "sync" button — evaluating it unguarded is arbitrary code execution in the process that loads the flow. As of 0.9.2 load_dsl therefore runs SafeSource.validate! first, and only source that matches the real DSL vocabulary (Vocabulary) with literal arguments gets past it.

Use SafeSource.validate or SafeSource.safe? directly to audit stored definitions without loading them — the answer is "would load_dsl accept this?", which is exactly what you want before a deploy tightens the allowlist.

Examples:

Reject before evaluating

Inquirex.load_dsl(customer.flow_dsl)          # validates, then evals
Inquirex.load_dsl(File.read("flow.rb"), unsafe: true)  # your own file: skip validation

Audit a table of stored definitions

Qualifier.find_each do |q|
  violations = Inquirex::SafeSource.validate(q.flow_dsl)
  puts "REJECT #{q.id}: #{violations.join("; ")}" if violations.any?
end

Raise the ceilings for an unusually large questionnaire

Inquirex::SafeSource.max_source_bytes = 256 * 1024
Inquirex::SafeSource.max_depth = 32

Defined Under Namespace

Modules: Vocabulary Classes: CallSpec, Validator

Constant Summary collapse

DEFAULT_MAX_SOURCE_BYTES =

Default ceiling on DSL source size, in bytes.

Real flows are tiny: the largest definition anywhere in this project or its downstream app — a 47-step loan application with three levels of composed rules — is under 6 KB. 64 KiB is an order of magnitude above that, comfortably fits a several-hundred-step questionnaire, and still bounds the work handed to Prism (and to any formatter the host runs on the same text), neither of which should be fed megabytes of hostile input.

64 * 1024
DEFAULT_MAX_DEPTH =

Default ceiling on AST nesting depth.

Measured against every flow in this gem's specs and examples and the downstream app's fixtures, the deepest legitimate construct needs 9 levels (define → block → ask → block → transitionallanyequals → literal). 24 leaves better than 2.5x headroom while keeping the validator's own recursion clear of a stack overflow triggered by all(all(all(...))) nested a million deep.

24

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.max_depthInteger

Ceiling on AST nesting depth, applied by validate and therefore by Inquirex.load_dsl.

Returns:

  • (Integer)


73
74
75
# File 'lib/inquirex/safe_source.rb', line 73

def max_depth
  @max_depth
end

.max_source_bytesInteger

Ceiling on DSL source size in bytes, applied by validate and therefore by Inquirex.load_dsl.

Returns:

  • (Integer)


67
68
69
# File 'lib/inquirex/safe_source.rb', line 67

def max_source_bytes
  @max_source_bytes
end

Class Method Details

.safe?(source, max_bytes: max_source_bytes, max_depth: self.max_depth) ⇒ Boolean

Returns true when the source is inside the allowlist.

Parameters:

  • source (String, nil)

    Inquirex DSL source

  • max_bytes (Integer) (defaults to: max_source_bytes)

    override the size ceiling for this call

  • max_depth (Integer) (defaults to: self.max_depth)

    override the depth ceiling for this call

Returns:

  • (Boolean)

    true when the source is inside the allowlist



92
93
94
# File 'lib/inquirex/safe_source.rb', line 92

def safe?(source, max_bytes: max_source_bytes, max_depth: self.max_depth)
  validate(source, max_bytes:, max_depth:).empty?
end

.validate(source, max_bytes: max_source_bytes, max_depth: self.max_depth) ⇒ Array<String>

Every reason the source would be rejected. An empty array means the source is inside the allowlist — it says nothing about whether the flow is semantically valid (unknown step reference, missing start), which only evaluation can decide.

Parameters:

  • source (String, nil)

    Inquirex DSL source

  • max_bytes (Integer) (defaults to: max_source_bytes)

    override the size ceiling for this call

  • max_depth (Integer) (defaults to: self.max_depth)

    override the depth ceiling for this call

Returns:

  • (Array<String>)

    "line N: reason" messages, empty when safe



84
85
86
# File 'lib/inquirex/safe_source.rb', line 84

def validate(source, max_bytes: max_source_bytes, max_depth: self.max_depth)
  Validator.new(source, max_bytes:, max_depth:).violations
end

.validate!(source, max_bytes: max_source_bytes, max_depth: self.max_depth) ⇒ String

Validates, and raises unless the source is inside the allowlist.

Parameters:

  • source (String, nil)

    Inquirex DSL source

  • max_bytes (Integer) (defaults to: max_source_bytes)

    override the size ceiling for this call

  • max_depth (Integer) (defaults to: self.max_depth)

    override the depth ceiling for this call

Returns:

  • (String)

    the source, unchanged, when it is safe

Raises:



103
104
105
106
107
108
# File 'lib/inquirex/safe_source.rb', line 103

def validate!(source, max_bytes: max_source_bytes, max_depth: self.max_depth)
  violations = validate(source, max_bytes:, max_depth:)
  raise Errors::UnsafeSourceError, violations unless violations.empty?

  source
end