Earlier this year, I worked on a bug where a query (as in CQS pattern) didn’t return the expected results when performing a specific search to find a record from a monetary amount.
Let me explain with an example what was going on and how I solved it.
The Starting Point
The search term was 5'. Why? Because the client application displayed money amounts in a pseudo-Swiss-German numeric format. The value 10000 would be displayed as 10'000 instead of 10’000 as it should.
Therefore, users in Switzerland, or in any other country for that matter, don’t really type the typographic apostrophe (’, you have to type ALT+0146 to get it…) but they will use the regular apostrophe (aka. dactylographic apostrophe or ').
The query pulled a list of items and we used EF6, Gridify (for pagination) and LINQ to write the query.
At that stage, nothing is in memory yet: the query still has to run on SQL Server.
The search above assumed the existence of a column Amount of type money, in the SQL Server database, where the record’s value was 65000.
The code looked like this (it isn’t the original code but something close to it):
| |
Where is the issue?
The Cause
In short, the stored amount contains no apostrophe, so a search term containing one can never match: 65000 doesn’t contain 5'. The search term has to be normalized before it reaches the query.
Let’s fix that!
The Solution
| |
I wrote the following tests:
| |
These were fine. But, I missed a few use cases:
| |
These failed. Why?
The regular expression only handled apostrophes with digits on both sides. To match the extra 4 tests, it had to become the following:
| |
This removes an apostrophe that follows a digit, or one that precedes a digit and doesn’t follow a word character.
In detail:
- First part:
(?<!\w)['’](?=\d)- This matches apostrophes before digits:
(?<!\w)- It is a negative lookbehind that asserts that what comes immediately before the current position is NOT a word character.?<!represents the negative lookbehind.\wmatches word characters: letters, digits and underscores, including non-ASCII ones by default in .NET.- This prevents matching apostrophes in the middle of words like
don't50.
['’]- Matches any ONE of these apostrophe characters:
'- Regular apostrophe (U+0027)’- Typographic/right single quotation mark (U+2019)
- Matches any ONE of these apostrophe characters:
(?=\d)- Positive lookahead?=asserts that what comes immediately after the apostrophe is a digit\dmatches any digit, including non-ASCII digits by default in .NET
- This matches apostrophes before digits:
|: the pipe character meansOR. The regex will match if EITHER the first part OR the second part matches to something- Second part:
(?<=\d)['’]- This matches apostrophes after digits:
(?<=\d)is a positive lookbehind that asserts that what comes immediately before the current position IS a digit. This ensures we only match apostrophes that follow numbers['’]works like the first part of the expression.
- This matches apostrophes after digits:
Testing StripNumericApostrophes
That prompted me to test the method StripNumericApostrophes. The use cases were:
| |
For my use case, I didn’t need to handle inputs like @'10 or 10'! because it wasn’t going to happen. It would have been useless tests.
Security Risks Evaluation
From my research, I came across a ReDoS (Regular Expression Denial of Service) risk, though the risk is relatively low with this pattern. However, I’d like to share the possible concerns that you need to be aware of.
ReDoS Risk Assessment
The regex (?<!\w)['’](?=\d)|(?<=\d)['’] is relatively safe because:
- it uses no nested quantifiers (
+,*,{n,m}) - it has no overlapping alternations that cause excessive backtracking
- For example, a
(.|\s)*pattern with a source string that is many spaces could end up with 2^N permutations.
- For example, a
- its lookbehinds and lookaheads are fixed-width
?<!and?<=check exactly 1 character behind so it is fixed-width?=checks exactly 1 character ahead so it is fixed-width
However, malicious input could still cause issues with very long strings (MB+ of text) that would take time to process. Indeed, Regex.Replace must scan the entire string character by character.
Recommended Mitigations
In the frontend application, the <input> element could use the maxlength attribute to limit this issue. In the backend application, we could also mitigate the issue with a string length check:
| |
Additional Best Practices
You could consider caching the compiled regex for better performance:
| |
Using this practice will depend on the number of searches performed on the frontend.
Caching could help, but that’s an optimization to consider once performance becomes a problem. It wasn’t used heavily in the application (only one endpoint).
Non-Regex Approach
If you’re very concerned about security, consider a character-by-character approach:
| |
This works much the same as the regex method.
Sources
- Timeout - Timeouts are built in to the .NET standard library through the System.Text.RegularExpressions.Regex MatchTimeout property (Wikipedia).
- Length limits - This is a standard security practice for input validation.
- RegexOptions.Compiled - Improves performance for frequently used patterns (Microsoft Learn).
- Try-catch - When a RegexMatchTimeoutException is thrown, the recommended pattern is to wait for a brief, random time interval after the exception is thrown before calling the matching method again, which applies when the time-out was set too low. For excessive backtracking, the recommendation is to abandon the attempt, inform the user and log the details (Microsoft Learn).
Conclusion
Search is a complex matter and we could go on well beyond this article. Using specialized services may help, but in this case, adding a new dependency was too much overhead, so we kept the regular expression.
Follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Credit: Photo by Marina Agrelo on Pexels (https://www.pexels.com/photo/person-using-a-calculator-5652294/).
