From 25662616011e064664cd42caa2a9a1778c0e1253 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:22:57 +0000 Subject: [PATCH] Fix false positives in PiiExposureExtractor Refined the `MASK_CALL_NEAR` and `PROJECTION` regexes to accommodate common benign serialization and omission patterns across web frameworks, including: - Lodash `omit(user, ...)` - Prisma `.omit` inside queries - Mongoose `.select('-email')` - `.toJSON()` overrides - Custom `anonymize*` and `sanitize*` functions - Data-transform methods like `UserPresenter.present(user)` Added 8 test snippets in `tests/test_pentest_regressions.py` under `test_benign_serializers_dont_trigger` to prevent future regressions. Updated `CHANGELOG.md` to reflect these improvements. --- CHANGELOG.md | 1 + CHANGELOG_UPDATE.txt | 1 + src/websec_validator/extractors/pii_exposure.py | 4 ++-- tests/test_pentest_regressions.py | 16 ++++++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG_UPDATE.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index db53af9..580fb95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +- Added PiiExposureExtractor exclusions for common benign serializers (omit, Prisma omit, Mongoose select, toJSON, anonymize, sanitize) to stop raw-entity false positives. ### Added diff --git a/CHANGELOG_UPDATE.txt b/CHANGELOG_UPDATE.txt new file mode 100644 index 0000000..09d7f98 --- /dev/null +++ b/CHANGELOG_UPDATE.txt @@ -0,0 +1 @@ +- Added PiiExposureExtractor exclusions for common benign serializers (omit, Prisma omit, Mongoose select, toJSON, anonymize, sanitize) to stop raw-entity false positives. diff --git a/src/websec_validator/extractors/pii_exposure.py b/src/websec_validator/extractors/pii_exposure.py index 73b4f59..c0df941 100644 --- a/src/websec_validator/extractors/pii_exposure.py +++ b/src/websec_validator/extractors/pii_exposure.py @@ -39,14 +39,14 @@ # returning a raw variable / a fresh ORM read straight to the client RES_RAW = re.compile(r"res\.(?:json|send)\s*\(\s*(?:await\s+)?[A-Za-z_$][\w$]*\s*\)" r"|res\.(?:json|send)\s*\(\s*await\s+[\w.]+\.(?:find|findOne|findById|findAll|get|query)\s*\(") -MASK_CALL_NEAR = re.compile(r"mask\w+\(|redact\w+\(|toPublic\w+\(|canViewFull\w+\(|\.serialize\(|toDto\(|\bDTO\b|pick\(", re.I) +MASK_CALL_NEAR = re.compile(r"mask\w+\(|redact\w+\(|toPublic\w+\(|canViewFull\w+\(|\.serialize\(|toDto\(|\bDTO\b|pick\(|omit\(|exclude\(|anonymize\w*\(|sanitize\w*\(|\.toJSON\(|\.present\(", re.I) TESTFILE = re.compile(r"(?:^|/)(?:tests?|__tests__|spec)/|\.(?:test|spec)\.", re.I) # A helper that masks a SECRET (connection string / password / token), not customer PII — wrong # category, and "defined but unused" on it is at most a lint nit. Excluded from the PII dead-control. SECRET_MASKER = re.compile(r"(?:mask|redact|scrub)\w*(?:Url|Uri|Dsn|Database|Conn|Connection|Secret|Password|Passwd|Token|Key|Cred)\w*", re.I) # inline object-projection (`.map(x => ({...}))` / a returned object literal) IS a serializer — a # raw-entity finding on a file that projects fields before responding is a false positive. -PROJECTION = re.compile(r"=>\s*\(\s*\{|\.map\s*\(\s*[\w$]*\s*=>|\bselect\s*:\s*\{|\binterface\s+\w+|\btype\s+\w+\s*=\s*\{", re.I) +PROJECTION = re.compile(r"=>\s*\(\s*\{|\.map\s*\(\s*[\w$]*\s*=>|\bselect\s*:\s*\{|\binterface\s+\w+|\btype\s+\w+\s*=\s*\{|\.select\(|\bomit\s*:\s*\{", re.I) class PiiExposureExtractor(Extractor): diff --git a/tests/test_pentest_regressions.py b/tests/test_pentest_regressions.py index 734194f..46e54e6 100644 --- a/tests/test_pentest_regressions.py +++ b/tests/test_pentest_regressions.py @@ -358,6 +358,22 @@ def test_dead_control_and_raw_entity_flagged(self): self.assertIn("maskContactPii", out["dead_controls"]) self.assertIn("raw-entity-pii-response", kinds) + + def test_benign_serializers_dont_trigger(self): + fps = { + "lodash_omit.js": "app.get('/user', async (req, res) => { const user = await User.findOne(); res.json(omit(user.toObject(), ['email', 'phone'])); });", + "prisma_omit.js": "app.get('/user', async (req, res) => { const user = await prisma.user.findUnique({ omit: { email: true, phone: true } }); res.json(user); });", + "mongoose_select.js": "app.get('/user', async (req, res) => { const user = await User.findById(id).select('-email -phone'); res.json(user); });", + "to_json.js": "app.get('/user', async (req, res) => { const user = await User.findOne(); res.json(user.toJSON()); }); // email", + "presenter.js": "app.get('/user', async (req, res) => { const user = await User.findOne(); res.json(UserPresenter.present(user)); }); // email", + "anonymize.js": "app.get('/user', async (req, res) => { const user = await User.findOne(); res.json(anonymizeUser(user)); }); // email", + "sanitize.js": "app.get('/user', async (req, res) => { const user = await User.findOne(); res.json(sanitizeUser(user)); }); // email", + "exclude.js": "app.get('/user', async (req, res) => { const user = await User.findOne(); res.json(exclude(user, ['email'])); });" + } + out = PiiExposureExtractor().extract(repo(fps), {}) + kinds = {f["kind"] for f in out["findings"]} + self.assertNotIn("raw-entity-pii-response", kinds, "Should not trigger on common benign serialization patterns") + def test_wired_masker_not_dead(self): out = PiiExposureExtractor().extract(repo({ "utils/pii.ts": self.MASKER,