Class: Inquirex::SafeSource::Validator

Inherits:
Object
  • Object
show all
Defined in:
lib/inquirex/safe_source/validator.rb

Overview

Default-deny AST allowlist for the Inquirex flow DSL.

The validator parses the source with Prism and walks it by recursive descent against an expected shape rather than by visiting every node and asking "is this one forbidden?". At each position — top-level statement, flow-block statement, step-block statement, argument, keyword value, Hash element — only the handful of node types the real DSL produces there are accepted, and anything else is a violation. A node type nobody thought about is therefore rejected by construction, which is the whole point: a blocklist of forbidden methods would be defeated by the first construct that was overlooked.

What that rules out, without needing to name any of it: system, backticks and %x, exec/spawn/fork, require/load, eval and the *_eval family, send/__send__/public_send/method, every constant reference other than the Inquirex entry point (so no File, IO, Dir, Kernel, ENV, ObjectSpace, Process, Object.const_get), instance, class and global variables, def/class/module, begin/rescue, at_exit, BEGIN/END, singleton definitions, __FILE__-style magic, assignments, conditionals, loops, string/symbol/regexp interpolation (including inside heredocs), splats and block-pass arguments.

Ruby blocks are accepted only where the DSL itself opens a nested scope (Vocabulary block:), and those blocks are validated statement by statement in turn. Every other block is rejected, which is why compute, a block-form default, fallback and an action's run cannot be used in safe mode: a compute block is indistinguishable from a payload.

Examples:

Validator.new("Inquirex.define { start :a }").violations # => []
Validator.new("system('id')").violations
# => ["line 1: the DSL must be a single `Inquirex.define` block"]

Constant Summary collapse

MAX_REPORTED =

Cap on reported violations, so a hostile 64 KiB payload cannot turn a validation error into a megabyte of flash message.

10
UNSAFE_HINT =

Appended to violations an author could legitimately have meant.

" — evaluate source you wrote yourself with `Inquirex.load_dsl(text, unsafe: true)`"
NODE_LABELS =

Node types worth naming explicitly in a violation message, because the humanized class name would not tell the author what they actually wrote.

{
  Prism::XStringNode                       => "a backtick or %x command",
  Prism::InterpolatedXStringNode           => "a backtick or %x command",
  Prism::InterpolatedStringNode            => "an interpolated string (interpolation is never allowed, not even in a heredoc)",
  Prism::InterpolatedSymbolNode            => "an interpolated symbol",
  Prism::InterpolatedRegularExpressionNode => "an interpolated regular expression",
  Prism::RegularExpressionNode             => "a regular expression",
  Prism::ConstantReadNode                  => "a constant reference",
  Prism::ConstantPathNode                  => "a constant reference",
  Prism::InstanceVariableReadNode          => "an instance variable",
  Prism::ClassVariableReadNode             => "a class variable",
  Prism::GlobalVariableReadNode            => "a global variable",
  Prism::DefNode                           => "a method definition",
  Prism::ClassNode                         => "a class definition",
  Prism::ModuleNode                        => "a module definition",
  Prism::SingletonClassNode                => "a singleton class definition",
  Prism::BeginNode                         => "a begin/rescue block",
  Prism::LambdaNode                        => "a lambda",
  Prism::BlockNode                         => "a block",
  Prism::BlockArgumentNode                 => "a block-pass (&) argument",
  Prism::SplatNode                         => "a splat (*) argument",
  Prism::AssocSplatNode                    => "a double-splat (**) argument",
  Prism::SourceFileNode                    => "__FILE__",
  Prism::SourceLineNode                    => "__LINE__",
  Prism::SourceEncodingNode                => "__ENCODING__",
  Prism::SelfNode                          => "self",
  Prism::PreExecutionNode                  => "a BEGIN block",
  Prism::PostExecutionNode                 => "an END block"
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(source, max_bytes: SafeSource::DEFAULT_MAX_SOURCE_BYTES, max_depth: SafeSource::DEFAULT_MAX_DEPTH) ⇒ Validator

Returns a new instance of Validator.

Parameters:

  • source (String, nil)

    Inquirex DSL source to validate

  • max_bytes (Integer) (defaults to: SafeSource::DEFAULT_MAX_SOURCE_BYTES)

    ceiling on source size

  • max_depth (Integer) (defaults to: SafeSource::DEFAULT_MAX_DEPTH)

    ceiling on AST nesting depth



81
82
83
84
85
86
87
# File 'lib/inquirex/safe_source/validator.rb', line 81

def initialize(source,
  max_bytes: SafeSource::DEFAULT_MAX_SOURCE_BYTES,
  max_depth: SafeSource::DEFAULT_MAX_DEPTH)
  @source = source
  @max_bytes = max_bytes
  @max_depth = max_depth
end

Instance Method Details

#violationsArray<String>

Every reason this source falls outside the allowlist.

Returns:

  • (Array<String>)

    "line N: reason" messages, empty when safe



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

def violations
  @violations ||= analyze.uniq.first(MAX_REPORTED)
end