@@ -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