Skip to content

Commit 379b771

Browse files
committed
fix(stealth): fix error handling correctness while preserving intentional output sanitization [CL-2]
- generateErrorObject: return original error as fallback instead of undefined - DOMException: preserve err.name (e.g. SecurityError) via constructor second arg - Object.getPropertyDescriptor: use Object.defineProperty with enumerable:false - Enhance test_stealth_records_js_calls with data quality assertions - Add test_legacy_records_with_call_stacks baseline test
1 parent 20a8445 commit 379b771

3 files changed

Lines changed: 89 additions & 46 deletions

File tree

Extension/src/stealth/error.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function generateErrorObject(
2323
(typeof err.name === "string" && err.name === "DOMException")
2424
) {
2525
// DOMException constructor takes (message, name) not (message, fileName)
26-
fakeError = new wrappedJS.DOMException(err.message);
26+
fakeError = new wrappedJS.DOMException(err.message, err.name);
2727
} else if (
2828
ErrorConstructor &&
2929
typeof ErrorConstructor === "function" &&
@@ -43,7 +43,7 @@ function generateErrorObject(
4343
} catch (error) {
4444
console.log("ERROR creation failed. Error was:" + error);
4545
}
46-
return fakeError;
46+
return fakeError || err;
4747
}
4848

4949
/*

Extension/src/stealth/instrument.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,23 @@ function serializeObject(
9292

9393
// Rough implementations of Object.getPropertyDescriptor and Object.getPropertyNames
9494
// See http://wiki.ecmascript.org/doku.php?id=harmony:extended_object_api
95-
Object.getPropertyDescriptor = function (subject, name) {
96-
if (subject === undefined) {
97-
throw new Error("Can't get property descriptor for undefined");
98-
}
99-
let pd = Object.getOwnPropertyDescriptor(subject, name);
100-
let proto = Object.getPrototypeOf(subject);
101-
while (pd === undefined && proto !== null) {
102-
pd = Object.getOwnPropertyDescriptor(proto, name);
103-
proto = Object.getPrototypeOf(proto);
104-
}
105-
return pd;
106-
};
95+
Object.defineProperty(Object, "getPropertyDescriptor", {
96+
enumerable: false,
97+
configurable: true,
98+
writable: false,
99+
value: function (subject, name) {
100+
if (subject === undefined) {
101+
throw new Error("Can't get property descriptor for undefined");
102+
}
103+
let pd = Object.getOwnPropertyDescriptor(subject, name);
104+
let proto = Object.getPrototypeOf(subject);
105+
while (pd === undefined && proto !== null) {
106+
pd = Object.getOwnPropertyDescriptor(proto, name);
107+
proto = Object.getPrototypeOf(proto);
108+
}
109+
return pd;
110+
},
111+
});
107112

108113
function updateCounterAndCheckIfOver(scriptUrl, symbol) {
109114
const key = scriptUrl + "|" + symbol;

test/test_stealth.py

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class TestStealthDetection:
9898
"""
9999

100100
@pytest.fixture(autouse=True)
101-
def set_tmpdir(self, tmp_path: Path):
101+
def set_tmpdir(self, tmp_path: Path) -> None:
102102
self.tmpdir = tmp_path
103103

104104
def _get_stealth_config(
@@ -140,60 +140,61 @@ def test_stealth_passes_all_detection_checks(self):
140140
assert results, "No detection results collected"
141141

142142
# navigator.webdriver should be hidden
143-
assert results.get("webdriver_flag") is True, (
144-
"navigator.webdriver was not hidden by stealth extension"
145-
)
143+
assert (
144+
results.get("webdriver_flag") is True
145+
), "navigator.webdriver was not hidden by stealth extension"
146146

147147
# Canvas functions should still appear native (no prototype pollution)
148-
assert results.get("canvas_functions_native") is True, (
149-
"Canvas functions are not native - stealth instrument should avoid prototype pollution"
150-
)
148+
assert (
149+
results.get("canvas_functions_native") is True
150+
), "Canvas functions are not native - stealth instrument should avoid prototype pollution"
151151

152152
# Storage functions should still appear native
153-
assert results.get("storage_functions_native") is True, (
154-
"Storage functions are not native - stealth instrument should avoid prototype pollution"
155-
)
153+
assert (
154+
results.get("storage_functions_native") is True
155+
), "Storage functions are not native - stealth instrument should avoid prototype pollution"
156156

157157
# Navigator getters should appear native
158-
assert results.get("navigator_native") is True, (
159-
"Navigator getters are not native - stealth instrument should avoid prototype pollution"
160-
)
158+
assert (
159+
results.get("navigator_native") is True
160+
), "Navigator getters are not native - stealth instrument should avoid prototype pollution"
161161

162162
# No OpenWPM globals should be exposed
163-
assert results.get("no_global_leaks") is True, (
164-
"OpenWPM globals detected (jsInstruments, instrumentFingerprintingApis, etc.)"
165-
)
163+
assert (
164+
results.get("no_global_leaks") is True
165+
), "OpenWPM globals detected (jsInstruments, instrumentFingerprintingApis, etc.)"
166166

167167
# Constructor properties should be preserved
168-
assert results.get("constructors_present") is True, (
169-
"Constructor properties missing on instrumented objects"
170-
)
168+
assert (
169+
results.get("constructors_present") is True
170+
), "Constructor properties missing on instrumented objects"
171171

172172
# Function.prototype.bind should not be tampered
173-
assert results.get("bind_integrity") is True, (
174-
"Function.prototype.bind integrity check failed"
175-
)
173+
assert (
174+
results.get("bind_integrity") is True
175+
), "Function.prototype.bind integrity check failed"
176176

177177
# Error stacks should not contain extension URLs
178-
assert results.get("clean_error_stacks") is True, (
179-
"Error stack traces contain moz-extension:// URLs"
180-
)
178+
assert (
179+
results.get("clean_error_stacks") is True
180+
), "Error stack traces contain moz-extension:// URLs"
181181

182182
# No extra properties added to prototypes
183-
assert results.get("no_extra_prototype_properties") is True, (
184-
"Extra instrumentation properties found on prototypes"
185-
)
183+
assert (
184+
results.get("no_extra_prototype_properties") is True
185+
), "Extra instrumentation properties found on prototypes"
186186

187187
# RTC functions should appear native
188-
assert results.get("rtc_native") is True, (
189-
"RTCPeerConnection functions are not native"
190-
)
188+
assert (
189+
results.get("rtc_native") is True
190+
), "RTCPeerConnection functions are not native"
191191

192192
def test_stealth_records_js_calls(self):
193193
"""Verify that the stealth extension actually records JS instrumentation data.
194194
195195
The detection tests above check that stealth is undetectable, but we also
196-
need to confirm it is actually capturing API calls to the database.
196+
need to confirm it is actually capturing API calls to the database
197+
with meaningful data (symbols, operations, values).
197198
"""
198199
manager_params, browser_params = self._get_stealth_config()
199200
db_path = manager_params.data_directory / "crawl-data.sqlite"
@@ -219,6 +220,43 @@ def test_stealth_records_js_calls(self):
219220
"in the javascript table"
220221
)
221222

223+
# Verify data quality: rows should have non-empty symbols and operations
224+
symbols = {row["symbol"] for row in rows}
225+
operations = {row["operation"] for row in rows}
226+
assert len(symbols) > 0, "No symbols recorded in JS instrumentation data"
227+
assert len(operations) > 0, "No operations recorded in JS instrumentation data"
228+
229+
# At least some rows should have non-empty values (proving data capture works)
230+
rows_with_values = [r for r in rows if r["value"] and r["value"] != ""]
231+
assert len(rows_with_values) > 0, (
232+
"Stealth extension recorded JS calls but no values — "
233+
"error handling may be discarding data"
234+
)
235+
236+
def test_legacy_records_with_call_stacks(self):
237+
"""Verify that legacy JS instrumentation records calls with full stack traces.
238+
239+
This serves as a baseline comparison: legacy mode captures call stacks
240+
that include extension URLs (which stealth intentionally sanitizes).
241+
"""
242+
manager_params, browser_params = self._get_legacy_config()
243+
db_path = manager_params.data_directory / "crawl-data.sqlite"
244+
structured_provider = SQLiteStorageProvider(db_path)
245+
manager = TaskManager(
246+
manager_params,
247+
browser_params,
248+
structured_provider,
249+
None,
250+
)
251+
252+
cs = CommandSequence(STEALTH_DETECTION_URL)
253+
cs.get(sleep=2)
254+
manager.execute_command_sequence(cs)
255+
manager.close()
256+
257+
rows = db_utils.get_javascript_entries(db_path, all_columns=True)
258+
assert len(rows) > 0, "Legacy JS instrumentation did not record any data"
259+
222260
def test_legacy_instrument_is_detectable(self):
223261
"""With legacy JS instrumentation, detection checks should catch it.
224262

0 commit comments

Comments
 (0)