parse-stack-next 5.7.1 → 5.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +163 -0
- data/bin/parse-console +108 -16
- data/examples/rag_chatbot.rb +5 -2
- data/lib/parse/agent/mcp_client.rb +46 -12
- data/lib/parse/agent/mcp_dispatcher.rb +2 -2
- data/lib/parse/agent.rb +0 -1
- data/lib/parse/client/body_builder.rb +12 -5
- data/lib/parse/client/logging.rb +23 -8
- data/lib/parse/client/request.rb +16 -4
- data/lib/parse/client.rb +34 -4
- data/lib/parse/console.rb +9 -3
- data/lib/parse/embeddings/image_fetch.rb +9 -1
- data/lib/parse/embeddings.rb +17 -4
- data/lib/parse/model/core/embed_managed.rb +41 -4
- data/lib/parse/model/core/querying.rb +0 -4
- data/lib/parse/model/object.rb +0 -1
- data/lib/parse/query/constraints.rb +61 -5
- data/lib/parse/query.rb +83 -8
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +1 -0
- data/lib/parse/terminal_safe.rb +138 -0
- data/lib/parse/webhooks.rb +29 -13
- metadata +2 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
# Neutralizes terminal control sequences in untrusted text before it is
|
|
6
|
+
# written to a terminal, a log record, or an IRB `inspect` line.
|
|
7
|
+
#
|
|
8
|
+
# Parse Server returns whatever a tenant stored. Any string that originates
|
|
9
|
+
# in the database, in a server error message, or in an LLM answer is
|
|
10
|
+
# attacker-influenced, and a raw ESC byte in that string is not inert once it
|
|
11
|
+
# reaches a TTY. It can clear the screen, rewrite lines the operator has
|
|
12
|
+
# already read, retitle the window, or (via OSC 52) write the attacker's
|
|
13
|
+
# payload into the system clipboard so the operator's next paste runs it.
|
|
14
|
+
# Bidirectional overrides are the same class of problem: they reorder a line
|
|
15
|
+
# so what the operator reads is not what the bytes say.
|
|
16
|
+
#
|
|
17
|
+
# This module is the SDK's single answer to that. It escapes rather than
|
|
18
|
+
# deletes, so the offending bytes stay visible in the output (rendered as
|
|
19
|
+
# `\e`, `\u202E`, and friends) and an operator can still see that
|
|
20
|
+
# something tried.
|
|
21
|
+
#
|
|
22
|
+
# It sanitizes *rendering*, never storage. `result.text`, `object.title`, and
|
|
23
|
+
# the parsed response body keep their exact bytes. Only the human-readable
|
|
24
|
+
# form built for a terminal or a log line runs through here.
|
|
25
|
+
#
|
|
26
|
+
# @example Rendering an untrusted value
|
|
27
|
+
# puts Parse::TerminalSafe.sanitize(post.title)
|
|
28
|
+
#
|
|
29
|
+
# @example A single-line log record. Newlines are escaped too, so a stored
|
|
30
|
+
# value cannot forge a second log entry.
|
|
31
|
+
# logger.warn "#{status} - #{Parse::TerminalSafe.sanitize_line(error)}"
|
|
32
|
+
module TerminalSafe
|
|
33
|
+
extend self
|
|
34
|
+
|
|
35
|
+
# Codepoint ranges neutralized on every call:
|
|
36
|
+
#
|
|
37
|
+
# - 0x00-0x08, 0x0B-0x1F: C0 controls except TAB (0x09) and LF (0x0A).
|
|
38
|
+
# This is where ESC (0x1B, the CSI/OSC/DCS introducer), BEL (0x07, the
|
|
39
|
+
# OSC string terminator), CR (0x0D, overwrite-the-line), and BS (0x08,
|
|
40
|
+
# erase-the-previous-character) live.
|
|
41
|
+
# - 0x7F: DEL.
|
|
42
|
+
# - 0x80-0x9F: C1 controls, the 8-bit forms of the same introducers
|
|
43
|
+
# (0x9B is CSI, 0x9D is OSC, 0x90 is DCS).
|
|
44
|
+
# - 0x061C: Arabic letter mark, an implicit bidirectional control that
|
|
45
|
+
# reorders a line exactly like the explicit marks below.
|
|
46
|
+
# - 0x200B-0x200F: zero-width space, non-joiner, joiner, LRM, RLM.
|
|
47
|
+
# - 0x2028-0x2029: line and paragraph separator. Widely treated as a line
|
|
48
|
+
# break by terminals, editors, and log readers, so leaving them intact
|
|
49
|
+
# would reintroduce the forged-record problem that escaping LF solves.
|
|
50
|
+
# A legitimate line break is LF, which is preserved.
|
|
51
|
+
# - 0x202A-0x202E: bidirectional embedding and override.
|
|
52
|
+
# - 0x2060-0x2064: word joiner and the invisible math operators.
|
|
53
|
+
# - 0x2066-0x2069: the bidirectional isolates.
|
|
54
|
+
# - 0xFEFF: zero-width no-break space (a BOM appearing mid-string).
|
|
55
|
+
#
|
|
56
|
+
# TAB and LF are deliberately absent: both are ordinary formatting in
|
|
57
|
+
# multi-line output. {#sanitize_line} escapes LF as well.
|
|
58
|
+
UNSAFE_RANGES = [
|
|
59
|
+
0x00..0x08, 0x0B..0x1F, 0x7F..0x9F,
|
|
60
|
+
0x061C..0x061C,
|
|
61
|
+
0x200B..0x200F, 0x2028..0x2029, 0x202A..0x202E,
|
|
62
|
+
0x2060..0x2064, 0x2066..0x2069,
|
|
63
|
+
0xFEFF..0xFEFF,
|
|
64
|
+
].freeze
|
|
65
|
+
|
|
66
|
+
# Built from codepoints rather than written as literal escapes so the
|
|
67
|
+
# source of this file stays free of the very bytes it defends against.
|
|
68
|
+
def self.build_pattern(ranges)
|
|
69
|
+
body = ranges.map { |r| format("\\u{%04X}-\\u{%04X}", r.first, r.last) }.join
|
|
70
|
+
Regexp.new("[#{body}]")
|
|
71
|
+
end
|
|
72
|
+
private_class_method :build_pattern
|
|
73
|
+
|
|
74
|
+
UNSAFE_RE = build_pattern(UNSAFE_RANGES)
|
|
75
|
+
|
|
76
|
+
# The same set plus LF, for output that must occupy exactly one line.
|
|
77
|
+
UNSAFE_LINE_RE = build_pattern(UNSAFE_RANGES + [0x0A..0x0A])
|
|
78
|
+
|
|
79
|
+
# Readable escapes for the controls an operator is most likely to see.
|
|
80
|
+
# Everything else falls back to `\xNN` or `\uNNNN`.
|
|
81
|
+
NAMED_ESCAPES = {
|
|
82
|
+
"\0" => "\\0",
|
|
83
|
+
"\a" => "\\a",
|
|
84
|
+
"\b" => "\\b",
|
|
85
|
+
"\n" => "\\n",
|
|
86
|
+
"\v" => "\\v",
|
|
87
|
+
"\f" => "\\f",
|
|
88
|
+
"\r" => "\\r",
|
|
89
|
+
"\e" => "\\e",
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
# Escape terminal control sequences in `str`, preserving newlines and tabs.
|
|
93
|
+
#
|
|
94
|
+
# @param str [String, #to_s, nil] untrusted text.
|
|
95
|
+
# @return [String] the same text with control characters escaped. Non-UTF-8
|
|
96
|
+
# and invalid-encoding input is coerced to UTF-8 first, so this never
|
|
97
|
+
# raises on a binary response body.
|
|
98
|
+
def sanitize(str)
|
|
99
|
+
escape(str, UNSAFE_RE)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Escape terminal control sequences and newlines, so untrusted text cannot
|
|
103
|
+
# forge additional lines. Use for anything written as a single log record
|
|
104
|
+
# or a single console line.
|
|
105
|
+
#
|
|
106
|
+
# @param str [String, #to_s, nil] untrusted text.
|
|
107
|
+
# @return [String]
|
|
108
|
+
def sanitize_line(str)
|
|
109
|
+
escape(str, UNSAFE_LINE_RE)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def escape(str, pattern)
|
|
115
|
+
s = coerce(str)
|
|
116
|
+
return s unless s.match?(pattern)
|
|
117
|
+
s.gsub(pattern) { |ch| escape_char(ch) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Force the input to valid UTF-8 without raising. A response body read off
|
|
121
|
+
# the wire can be ASCII-8BIT, and a truncated multi-byte sequence is not
|
|
122
|
+
# valid UTF-8. Either would make `match?` raise ArgumentError, which is
|
|
123
|
+
# exactly the wrong outcome for a defensive sanitizer.
|
|
124
|
+
def coerce(str)
|
|
125
|
+
s = str.is_a?(String) ? str : str.to_s
|
|
126
|
+
s = s.dup.force_encoding(Encoding::UTF_8) unless s.encoding == Encoding::UTF_8
|
|
127
|
+
s = s.scrub("�") unless s.valid_encoding?
|
|
128
|
+
s
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def escape_char(ch)
|
|
132
|
+
named = NAMED_ESCAPES[ch]
|
|
133
|
+
return named if named
|
|
134
|
+
cp = ch.ord
|
|
135
|
+
cp <= 0xFF ? format("\\x%02X", cp) : format("\\u%04X", cp)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/parse/webhooks.rb
CHANGED
|
@@ -11,6 +11,7 @@ require "active_model/serializers/json"
|
|
|
11
11
|
require "rack"
|
|
12
12
|
require "ostruct"
|
|
13
13
|
require_relative "client"
|
|
14
|
+
require_relative "terminal_safe"
|
|
14
15
|
# Note: Do not require "stack" here - this file is loaded from stack.rb
|
|
15
16
|
# and adding that require would create a circular dependency.
|
|
16
17
|
require_relative "model/object"
|
|
@@ -326,7 +327,10 @@ module Parse
|
|
|
326
327
|
# @param error [StandardError] the raised error.
|
|
327
328
|
# @return [void]
|
|
328
329
|
def report_handler_error(type, error)
|
|
329
|
-
|
|
330
|
+
# The handler's message is application-authored but routinely quotes the
|
|
331
|
+
# payload that triggered it, which is caller-controlled.
|
|
332
|
+
warn "[Parse::Webhooks] #{type} handler raised #{error.class}: " \
|
|
333
|
+
"#{Parse::TerminalSafe.sanitize_line(error.message)}; " \
|
|
330
334
|
"continuing with the remaining handlers " \
|
|
331
335
|
"(Parse::Webhooks.abort_after_callbacks_on_error is false)"
|
|
332
336
|
return unless defined?(ActiveSupport::Notifications)
|
|
@@ -444,7 +448,6 @@ module Parse
|
|
|
444
448
|
payload.instance_variable_set(:@ruby_initiated, ruby_initiated)
|
|
445
449
|
trusted_ruby_initiated = ruby_initiated && (payload.master? == true)
|
|
446
450
|
else
|
|
447
|
-
ruby_initiated = false
|
|
448
451
|
trusted_ruby_initiated = false
|
|
449
452
|
end
|
|
450
453
|
|
|
@@ -672,9 +675,9 @@ module Parse
|
|
|
672
675
|
# record contents/tokens, and the rest of this file routes log output
|
|
673
676
|
# through the same redactor.
|
|
674
677
|
warn "[Parse::Webhooks] afterSave #{phase} callback raised for " \
|
|
675
|
-
"#{obj.class}##{obj.id} -- the object is
|
|
676
|
-
"logging and continuing: #{e.class}: " \
|
|
677
|
-
"#{Parse::Middleware::BodyBuilder.redact(e.message)}"
|
|
678
|
+
"#{obj.class}##{Parse::TerminalSafe.sanitize_line(obj.id)} -- the object is " \
|
|
679
|
+
"already persisted; logging and continuing: #{e.class}: " \
|
|
680
|
+
"#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(e.message))}"
|
|
678
681
|
nil
|
|
679
682
|
end
|
|
680
683
|
|
|
@@ -854,20 +857,29 @@ module Parse
|
|
|
854
857
|
begin
|
|
855
858
|
payload = Parse::Webhooks::Payload.new(body_str, webhook_class)
|
|
856
859
|
rescue => e
|
|
857
|
-
warn "Invalid webhook payload format: #{e}"
|
|
860
|
+
warn "Invalid webhook payload format: #{Parse::TerminalSafe.sanitize_line(e.to_s)}"
|
|
858
861
|
response.write error("Invalid payload format. Should be valid JSON.")
|
|
859
862
|
return response.finish
|
|
860
863
|
end
|
|
861
864
|
|
|
862
865
|
if self.logging.present?
|
|
866
|
+
# Everything interpolated below arrives in the webhook request body:
|
|
867
|
+
# the trigger/function names, the object id, and the whole payload are
|
|
868
|
+
# caller-controlled, and these lines go to the app server's console.
|
|
869
|
+
# Escape control sequences so a stored value cannot drive the terminal
|
|
870
|
+
# of whoever is tailing the log.
|
|
863
871
|
if payload.trigger?
|
|
864
|
-
puts "[Webhooks::Request] --> #{payload.trigger_name}
|
|
872
|
+
puts "[Webhooks::Request] --> #{Parse::TerminalSafe.sanitize_line(payload.trigger_name)} " \
|
|
873
|
+
"#{Parse::TerminalSafe.sanitize_line(payload.parse_class)}:" \
|
|
874
|
+
"#{Parse::TerminalSafe.sanitize_line(payload.parse_id)}"
|
|
865
875
|
elsif payload.function?
|
|
866
|
-
puts "[ParseWebhooks Request] --> Function #{payload.function_name}"
|
|
876
|
+
puts "[ParseWebhooks Request] --> Function #{Parse::TerminalSafe.sanitize_line(payload.function_name)}"
|
|
867
877
|
end
|
|
868
878
|
if self.logging == :debug
|
|
869
879
|
puts "[Webhooks::Payload] ----------------------------"
|
|
870
|
-
puts Parse::
|
|
880
|
+
puts Parse::TerminalSafe.sanitize(
|
|
881
|
+
Parse::Middleware::BodyBuilder.redact(payload.as_json.to_json)
|
|
882
|
+
)
|
|
871
883
|
puts "----------------------------------------------------\n"
|
|
872
884
|
end
|
|
873
885
|
end
|
|
@@ -892,14 +904,14 @@ module Parse
|
|
|
892
904
|
else
|
|
893
905
|
if self.logging.present?
|
|
894
906
|
puts "[Webhooks] --> Could not find mapping route for " \
|
|
895
|
-
"#{Parse::Middleware::BodyBuilder.redact(payload.to_json)}"
|
|
907
|
+
"#{Parse::TerminalSafe.sanitize_line(Parse::Middleware::BodyBuilder.redact(payload.to_json))}"
|
|
896
908
|
end
|
|
897
909
|
end
|
|
898
910
|
|
|
899
911
|
result = true if result.nil?
|
|
900
912
|
if self.logging.present?
|
|
901
913
|
puts "[Webhooks::Response] ----------------------------"
|
|
902
|
-
puts success(result)
|
|
914
|
+
puts Parse::TerminalSafe.sanitize(success(result))
|
|
903
915
|
puts "----------------------------------------------------\n"
|
|
904
916
|
end
|
|
905
917
|
response.write success(result)
|
|
@@ -910,9 +922,13 @@ module Parse
|
|
|
910
922
|
return response.finish
|
|
911
923
|
rescue Parse::Webhooks::ResponseError, ActiveModel::ValidationError => e
|
|
912
924
|
if payload.trigger?
|
|
913
|
-
puts "[Webhooks::ResponseError] >> #{payload.trigger_name}
|
|
925
|
+
puts "[Webhooks::ResponseError] >> #{Parse::TerminalSafe.sanitize_line(payload.trigger_name)} " \
|
|
926
|
+
"#{Parse::TerminalSafe.sanitize_line(payload.parse_class)}:" \
|
|
927
|
+
"#{Parse::TerminalSafe.sanitize_line(payload.parse_id)}: " \
|
|
928
|
+
"#{Parse::TerminalSafe.sanitize_line(e.to_s)}"
|
|
914
929
|
elsif payload.function?
|
|
915
|
-
puts "[Webhooks::ResponseError] >> #{payload.function_name}:
|
|
930
|
+
puts "[Webhooks::ResponseError] >> #{Parse::TerminalSafe.sanitize_line(payload.function_name)}: " \
|
|
931
|
+
"#{Parse::TerminalSafe.sanitize_line(e.to_s)}"
|
|
916
932
|
end
|
|
917
933
|
response.write error(e.to_s)
|
|
918
934
|
return response.finish
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parse-stack-next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.7.
|
|
4
|
+
version: 5.7.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Curtin
|
|
@@ -422,6 +422,7 @@ files:
|
|
|
422
422
|
- lib/parse/stack/railtie.rb
|
|
423
423
|
- lib/parse/stack/tasks.rb
|
|
424
424
|
- lib/parse/stack/version.rb
|
|
425
|
+
- lib/parse/terminal_safe.rb
|
|
425
426
|
- lib/parse/two_factor_auth.rb
|
|
426
427
|
- lib/parse/two_factor_auth/user_extension.rb
|
|
427
428
|
- lib/parse/vector_search.rb
|