You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Regex allow_list: a backreference in a later term points at an earlier term's group #2433
With allow_list_match="regex", _remove_allow_list joins all terms into one pattern with "|".join(allow_list) (analyzer_engine.py:467). The capture groups then get numbers across the whole list. A backreference in a later term now points at a group of an earlier term. That term no longer matches its own text, and the result stays in the output with no error.
$ .venv/bin/python presidio_allowlist_repro.py
alone 1: ['AA-AA']
alone 2: ['John Smith']
together: ['AA-AA']
# presidio-analyzer from main at 17d1946, Python 3.14.7, regex 2026.9.29
# not run: analyze() end to end with an NLP engine; it calls _remove_allow_list at line 310
Expected behavior
together: []. The analyze docstring says that with regex, "results which match with any regex condition in the allow_list would be allowed and not be returned as potential PII." AA-AA matches the second condition.
Additional context
Wrapping each term in (?:...) does not help, because the group numbers still shift. Since the module already imports regex, a branch reset fixes it in the join itself: "(?|" + "|".join(allow_list) + ")" numbers the groups per alternative, and on the case above it matches AA-AA and John Smith and still rejects AA-BB. Compiling each term on its own also works, but it costs one timeout-guarded search per term per result. #2061 fixed another side effect of the same join.
If the branch reset works for you, I'd send it as a PR with this case as a test.
I ran your case through analyze() with two pattern recognizers on main at 17d1946, and it behaves the same end to end. The order matters as well. [r"^(John) Smith$", r"^(\w\w)-\1$"] keeps AA-AA, and the reverse order keeps nothing.
Before you send the branch reset, there are terms it can't compile. A term with its own top-level | becomes separate branches, and a call like (?1) becomes ambiguous once another term has a group 1. Using regex 2026.9.29 directly, with the engine's flags and .search():
Terms
Text
Today
(?| join
Each term alone
^(\d\d)-\1$|^(\w\w)=\2$
AA=AA
match
invalid group reference
match
^(John)$, ^(\d\d)-\1$|^(\w\w)=\2$
AA=AA
no match
invalid group reference
match
^(John)$, ^(\w\w)-(?1)$
AA-BB
no match
ambiguous group reference
match
The first row is the one to watch, since a single-term list that works today would stop working. The compile is outside the try, so from reading the code I'd expect that to surface as an exception from analyze(), though I didn't patch it in and run that.
Compiling each term once and checking them in turn matched in every case I tried, at the per-term search cost you mentioned. I haven't measured that cost.
@jamesconwaydev your three rows reproduce. Wrapping each term as (?:{t}) inside the reset fixes the first two, but it fails open: a term with a stray ) breaks out of its wrapper, compiles, and allow-lists every result.
# regex 2026.9.29, Python 3.12, flags DOTALL|MULTILINE|IGNORECASE, each pattern compiled once, 50 non-matching results
20 terms: joined 134 us per-term 3088 us
100 terms: joined 144 us per-term 14603 us
# regex 2022.10.31, Python 3.12.3, same flags; wrapped = "(?|" + "|".join(f"(?:{t})" for t in terms) + ")"
^John$, x)|(. 4111 1111 1111 1111 today: unbalanced parenthesis wrapped: match
^a b$, (?x)^zzz$ a b wrapped: no match per-term: match
So my pick is per-term search, about 3 ms per 50 results at 20 terms, with your rows and the breakout term as tests. If a maintainer is fine with that direction, I'll open the PR.
Activity
jamesconwaydev commentedon Oct 31, 2026
I ran your case through
analyze()with two pattern recognizers on main at 17d1946, and it behaves the same end to end. The order matters as well.[r"^(John) Smith$", r"^(\w\w)-\1$"]keepsAA-AA, and the reverse order keeps nothing.Before you send the branch reset, there are terms it can't compile. A term with its own top-level
|becomes separate branches, and a call like(?1)becomes ambiguous once another term has a group 1. Usingregex2026.9.29 directly, with the engine's flags and.search():(?|join^(\d\d)-\1$|^(\w\w)=\2$AA=AAinvalid group reference^(John)$,^(\d\d)-\1$|^(\w\w)=\2$AA=AAinvalid group reference^(John)$,^(\w\w)-(?1)$AA-BBambiguous group referenceThe first row is the one to watch, since a single-term list that works today would stop working. The compile is outside the
try, so from reading the code I'd expect that to surface as an exception fromanalyze(), though I didn't patch it in and run that.Compiling each term once and checking them in turn matched in every case I tried, at the per-term search cost you mentioned. I haven't measured that cost.
chrikrah commentedon Nov 2, 2026
@jamesconwaydev your three rows reproduce. Wrapping each term as
(?:{t})inside the reset fixes the first two, but it fails open: a term with a stray)breaks out of its wrapper, compiles, and allow-lists every result.So my pick is per-term search, about 3 ms per 50 results at 20 terms, with your rows and the breakout term as tests. If a maintainer is fine with that direction, I'll open the PR.