Skip to content

Commit 96b43d5

Browse files
committed
Improves deregistering datastore options validation
1 parent 5f926ff commit 96b43d5

2 files changed

Lines changed: 559 additions & 0 deletions

File tree

lib/msf/core/module_data_store.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def initialize(m)
1515
super()
1616

1717
@_module = m
18+
@deregistered_keys = []
1819
end
1920

2021
#
@@ -27,6 +28,60 @@ def copy
2728
new_instance
2829
end
2930

31+
# Grab any available deregistered_keys, otherwise return a blank array
32+
#
33+
# @param [Msf::DataStore] other The other datastore to copy state from
34+
# @return [Msf::DataStore] the current datastore instance
35+
def copy_state(other)
36+
super
37+
incoming = other.respond_to?(:deregistered_keys, true) ? other.deregistered_keys : []
38+
incoming.each do |key|
39+
@deregistered_keys << key unless @deregistered_keys.any? { |k| k.casecmp?(key) }
40+
end
41+
42+
# Strip any deregistered keys that may have been written into @user_defined
43+
# by the parent's copy_state (e.g. during reverse_merge! which calls
44+
# copy_state with a plain DataStore result that contains all merged values).
45+
@deregistered_keys.each do |key|
46+
k = find_key_case(key)
47+
@user_defined.delete(k)
48+
end
49+
50+
# Need self here to return datastore instance from parent
51+
self
52+
end
53+
54+
# Track deregistered keys
55+
#
56+
# @param [String] name
57+
# @return [nil]
58+
def remove_option(name)
59+
super
60+
# Resolve to the canonical cased key that the datastore actually uses so
61+
# that filtering later works correctly regardless of what case or alias the
62+
# caller passed to deregister_options.
63+
canonical = find_key_case(name)
64+
@deregistered_keys << canonical unless @deregistered_keys.any? { |k| k.casecmp?(canonical) }
65+
66+
# Need nil here to return original nil value from parent
67+
nil
68+
end
69+
70+
# Imports options and clears any matching keys from the deregistered list.
71+
# This allows a module to call deregister_options followed by register_options
72+
# for the same key and ensure we remove the re-registered option from the
73+
# tracked @deregistered_keys
74+
#
75+
# @param [Msf::OptionContainer] options
76+
# @param [String, nil] imported_by
77+
# @param [Boolean] overwrite
78+
def import_options(options, imported_by = nil, overwrite = true)
79+
options.each_option do |name, _option|
80+
@deregistered_keys.delete_if { |k| k.casecmp?(name) }
81+
end
82+
super
83+
end
84+
3085
# Search for a value within the current datastore, taking into consideration any registered aliases, fallbacks, etc.
3186
# If a value is not present in the current datastore, the global parent store will be referenced instead
3287
#
@@ -36,6 +91,9 @@ def search_for(key)
3691
k = find_key_case(key)
3792
return search_result(:user_defined, @user_defined[k]) if @user_defined.key?(k)
3893

94+
# If the module has not registered the key then return early as it has been deregistered
95+
return search_result(:not_found, nil) if should_filter_key?(key)
96+
3997
# Preference globally set values over a module's option default
4098
framework_datastore_search = search_framework_datastore(key)
4199
return framework_datastore_search if framework_datastore_search.found? && !framework_datastore_search.default?
@@ -61,8 +119,60 @@ def search_for(key)
61119
search_framework_datastore(k)
62120
end
63121

122+
# Override the write path so that keys which were explicitly deregistered on
123+
# the owning module are dropped.
124+
#
125+
# Compatible and functions as normal when:
126+
# - the key has never been deregistered via deregister_options
127+
# - there is no associated module (@_module is nil)
128+
#
129+
# @param [String] key
130+
# @param [Object] value
131+
def []=(key, value)
132+
return if should_filter_key?(key)
133+
134+
super
135+
end
136+
137+
# Override merge! so that when merging a DataStore (which writes directly to
138+
# @user_defined, bypassing []=), any deregistered keys are stripped out
139+
# afterward. This prevents a caller from injecting a deregistered option's
140+
# value by merging a datastore that contains it.
141+
#
142+
# When merging a plain Hash the parent's merge! already routes through []=,
143+
# so deregistered keys are filtered at that point and no extra work is needed.
144+
#
145+
# @param [Msf::DataStore, Hash] other
146+
# @return [Msf::ModuleDataStore]
147+
def merge!(other)
148+
super
149+
if other.is_a?(DataStore)
150+
@deregistered_keys.each do |key|
151+
k = find_key_case(key)
152+
@user_defined.delete(k)
153+
@options.delete_if { |opt_name, _| opt_name.casecmp?(k) }
154+
@aliases.delete_if { |_, v| v.casecmp?(k) }
155+
end
156+
end
157+
self
158+
end
159+
64160
protected
65161

162+
attr_reader :deregistered_keys
163+
164+
# Returns true when the write should be silently dropped because the key
165+
# was explicitly deregistered via deregister_options.
166+
# Normalises the incoming key via find_key_case so that differently-cased
167+
# references to the same key are correctly matched against @deregistered_keys.
168+
#
169+
# @param [String] key
170+
# @return [Boolean]
171+
def should_filter_key?(key)
172+
canonical = find_key_case(key)
173+
@deregistered_keys.any? { |deregistered| deregistered.casecmp?(canonical) }
174+
end
175+
66176
# Search the framework datastore
67177
#
68178
# @param [String] key The key to search for

0 commit comments

Comments
 (0)