Skip to content

Commit c79f067

Browse files
wip
1 parent 7bd01d9 commit c79f067

File tree

7 files changed

+445
-4
lines changed

7 files changed

+445
-4
lines changed

docs/SECURITY.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Security
2+
3+
This document describes security measures implemented in Tab Modifier.
4+
5+
## ReDoS (Regular Expression Denial of Service) Protection
6+
7+
### Background
8+
9+
Tab Modifier allows users to create rules with regular expressions to match URLs. User-controlled regex patterns can potentially be exploited to cause ReDoS attacks, where malicious regex patterns with catastrophic backtracking can freeze the browser tab.
10+
11+
### Implementation
12+
13+
We've implemented multi-layered protection against ReDoS attacks:
14+
15+
#### 1. Pattern Validation (`src/common/regex-safety.ts`)
16+
17+
The `_isRegexPatternSafe()` function validates regex patterns before execution:
18+
19+
- **Nested Quantifiers**: Blocks patterns like `(a+)+`, `(a*)*`, `(a{1,5})+` that cause exponential backtracking
20+
- **Consecutive Quantifiers**: Blocks patterns like `a**`, `a+*` that are invalid or dangerous
21+
- **Overlapping Alternatives**: Blocks patterns like `(a|a)*`, `(x+|x+y+)*` that create unnecessary backtracking
22+
- **Length Limits**: Rejects patterns longer than 1000 characters
23+
- **Syntax Validation**: Ensures the pattern is a valid regex
24+
25+
#### 2. Safe Execution
26+
27+
The `_safeRegexTestSync()` function:
28+
1. Validates the pattern before execution
29+
2. Catches and logs any execution errors
30+
3. Returns `false` for unsafe patterns instead of executing them
31+
32+
### Usage
33+
34+
Instead of using `new RegExp(pattern).test(url)` directly, always use:
35+
36+
```typescript
37+
import { _safeRegexTestSync } from './regex-safety.ts';
38+
39+
// Safe regex execution
40+
const matches = _safeRegexTestSync(userPattern, url);
41+
```
42+
43+
### Examples of Blocked Patterns
44+
45+
**Dangerous patterns that are blocked:**
46+
```regex
47+
(a+)+ # Nested quantifiers
48+
(a*)* # Nested quantifiers
49+
(a|a)* # Overlapping alternatives
50+
(x+|x+y+)* # Overlapping alternatives with quantifiers
51+
a++ # Consecutive quantifiers
52+
```
53+
54+
**Safe patterns that are allowed:**
55+
```regex
56+
example\.com # Simple literal match
57+
^https://[a-z]+\.example\.com # Character classes with quantifiers
58+
[0-9]{1,4} # Bounded quantifiers
59+
(?!MOUNCE).*version= # Negative lookahead
60+
```
61+
62+
### Testing
63+
64+
Comprehensive tests are in `src/common/regex-safety.test.js` covering:
65+
- Safe pattern validation
66+
- Dangerous pattern detection and blocking
67+
- Real-world regex patterns from existing rules
68+
- Edge cases and error handling
69+
70+
### Best Practices
71+
72+
1. **Prefer simpler detection methods** when possible:
73+
- Use `CONTAINS` for substring matching
74+
- Use `STARTS_WITH` / `ENDS_WITH` for prefix/suffix matching
75+
- Use `EXACT` for exact matching
76+
- Use `REGEX` only when pattern matching is truly needed
77+
78+
2. **Write safe regex patterns**:
79+
- Avoid nested quantifiers
80+
- Use bounded quantifiers (`{1,5}`) instead of unbounded (`*`, `+`)
81+
- Keep patterns as simple as possible
82+
- Test patterns with the safety validator
83+
84+
3. **Document complex patterns**:
85+
- Add comments explaining what the pattern matches
86+
- Include test cases for complex patterns
87+
88+
## Missing Regex Anchors in URL/Title Matchers
89+
90+
### Background
91+
92+
The `url_matcher` and `title_matcher` features allow users to extract parts of URLs and page titles using regular expressions with capture groups. When regex patterns don't use anchors (`^` for start, `$` for end), they can match anywhere in the input string, potentially causing unexpected behavior.
93+
94+
### Context: Not a Critical Security Issue
95+
96+
In Tab Modifier, `url_matcher` and `title_matcher` are used to **extract content** for display in tab titles, not for security-critical validation like URL redirection or authentication. Therefore, missing anchors here represent a **usability concern** rather than a security vulnerability.
97+
98+
However, to prevent unexpected matches and follow security best practices, we recommend using anchored patterns.
99+
100+
### Recommendations
101+
102+
#### Use Anchors When Possible
103+
104+
**Unanchored pattern (may match unexpectedly):**
105+
```regex
106+
https:\/\/example\.com\/(.+)
107+
# Could match: "evil.com?redirect=https://example.com/test"
108+
```
109+
110+
**Anchored pattern (matches precisely):**
111+
```regex
112+
^https:\/\/example\.com\/(.+)
113+
# Only matches URLs starting with https://example.com/
114+
```
115+
116+
#### Examples of Safe Patterns
117+
118+
**URL Matcher for GitHub repositories:**
119+
```regex
120+
^https:\/\/github\.com\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)
121+
```
122+
123+
**URL Matcher for query parameters:**
124+
```regex
125+
[?&]date=([0-9]{4}-[0-9]{2}-[0-9]{2})
126+
# Note: This doesn't need ^ anchor as we're matching a specific parameter
127+
```
128+
129+
**Title Matcher with full anchors:**
130+
```regex
131+
^[a-z]*@gmail\.com$
132+
```
133+
134+
#### When Anchors Are Optional
135+
136+
For patterns that specifically target **substrings** (like query parameters or path segments), anchors may not be needed:
137+
138+
```regex
139+
# Extracting query parameter - no anchor needed
140+
[?&]id=([0-9]+)
141+
142+
# Extracting date from URL - no anchor needed
143+
date=([0-9]{4}-[0-9]{2}-[0-9]{2})
144+
```
145+
146+
### Implementation
147+
148+
The regex safety checks in `src/content.js` already validate patterns before execution. To enhance this:
149+
150+
1. **Pattern Validation**: The `isRegexSafe()` function checks for dangerous patterns
151+
2. **Safe Execution**: The `createSafeRegex()` function creates validated regex instances
152+
3. **Error Handling**: All regex operations are wrapped in try-catch blocks
153+
154+
### Best Practices for URL/Title Matchers
155+
156+
1. **Use anchors (`^`, `$`) when matching complete URLs or titles**
157+
```regex
158+
✅ ^https:\/\/example\.com\/path$
159+
❌ https:\/\/example\.com\/path
160+
```
161+
162+
2. **Be specific with your patterns**
163+
```regex
164+
✅ ^https:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+
165+
❌ .+github.com.+
166+
```
167+
168+
3. **Use character classes instead of wildcards when possible**
169+
```regex
170+
✅ [A-Za-z0-9_-]+
171+
❌ .+
172+
```
173+
174+
4. **Test your patterns with the regex safety validator**
175+
- Patterns are automatically validated before use
176+
- Check browser console for validation warnings
177+
178+
5. **Consider the use case**
179+
- For extracting specific parts: anchors may be optional
180+
- For matching the entire URL/title: always use anchors
181+
182+
### References
183+
184+
- [OWASP: Regular Expression Denial of Service](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)
185+
- [Semgrep Rule: detect-non-literal-regexp](https://semgrep.dev/r/javascript.lang.security.audit.detect-non-literal-regexp)
186+
- [CodeQL: Missing Regular Expression Anchor](https://codeql.github.com/codeql-query-help/javascript/js-regex-missing-anchor/)
187+
- [OWASP: Server Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)

src/common/regex-safety.test.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, expect, it, vi, beforeEach } from 'vitest';
2+
import { _isRegexPatternSafe, _safeRegexTestSync } from './regex-safety';
3+
4+
describe('Regex Safety', () => {
5+
beforeEach(() => {
6+
vi.clearAllMocks();
7+
// Spy on console.warn to verify warnings are logged
8+
vi.spyOn(console, 'warn').mockImplementation(() => {});
9+
});
10+
11+
describe('_isRegexPatternSafe', () => {
12+
it('should accept safe simple patterns', () => {
13+
expect(_isRegexPatternSafe('example\\.com')).toBe(true);
14+
expect(_isRegexPatternSafe('https://[a-z]+\\.example\\.com')).toBe(true);
15+
expect(_isRegexPatternSafe('^https://example\\.com/path$')).toBe(true);
16+
expect(_isRegexPatternSafe('[0-9]{1,4}')).toBe(true);
17+
});
18+
19+
it('should accept complex but safe patterns from existing tests', () => {
20+
// From biblegateway test
21+
expect(
22+
_isRegexPatternSafe(
23+
'https:\\/\\/www\\.biblegateway\\.com\\/passage\\/\\?search=.*version=(?!MOUNCE)(?!.*;).*'
24+
)
25+
).toBe(true);
26+
27+
// From jira test (without the catastrophic .*? at the start)
28+
expect(
29+
_isRegexPatternSafe(
30+
'furybee.atlassian.net\\/jira\\/software\\/c\\/projects\\/([a-zA-Z]{1,5})\\/boards\\/([0-9]{1,4})(\\?.*)?$'
31+
)
32+
).toBe(true);
33+
});
34+
35+
it('should reject patterns with nested quantifiers', () => {
36+
expect(_isRegexPatternSafe('(a+)+')).toBe(false);
37+
expect(_isRegexPatternSafe('(a*)*')).toBe(false);
38+
expect(_isRegexPatternSafe('(a+)*')).toBe(false);
39+
expect(_isRegexPatternSafe('(a{1,5})+')).toBe(false);
40+
expect(_isRegexPatternSafe('(x+|y+)+')).toBe(false);
41+
});
42+
43+
it('should reject patterns with consecutive quantifiers', () => {
44+
expect(_isRegexPatternSafe('a+++')).toBe(false);
45+
expect(_isRegexPatternSafe('a**')).toBe(false);
46+
expect(_isRegexPatternSafe('a+*')).toBe(false);
47+
expect(_isRegexPatternSafe('a?+')).toBe(false);
48+
});
49+
50+
it('should reject patterns with overlapping alternatives', () => {
51+
expect(_isRegexPatternSafe('(a|a)*')).toBe(false);
52+
expect(_isRegexPatternSafe('(x+|x+y+)*')).toBe(false);
53+
});
54+
55+
it('should reject invalid regex patterns', () => {
56+
expect(_isRegexPatternSafe('[')).toBe(false);
57+
expect(_isRegexPatternSafe('((')).toBe(false);
58+
expect(_isRegexPatternSafe('*')).toBe(false);
59+
});
60+
61+
it('should reject empty or non-string patterns', () => {
62+
expect(_isRegexPatternSafe('')).toBe(false);
63+
expect(_isRegexPatternSafe(null)).toBe(false);
64+
expect(_isRegexPatternSafe(undefined)).toBe(false);
65+
});
66+
67+
it('should reject excessively long patterns', () => {
68+
const longPattern = 'a'.repeat(1001);
69+
expect(_isRegexPatternSafe(longPattern)).toBe(false);
70+
});
71+
});
72+
73+
describe('_safeRegexTestSync', () => {
74+
it('should match safe patterns correctly', () => {
75+
expect(_safeRegexTestSync('example\\.com', 'https://example.com/path')).toBe(true);
76+
expect(_safeRegexTestSync('example\\.com', 'https://other.com/path')).toBe(false);
77+
expect(_safeRegexTestSync('^https://example\\.com', 'https://example.com/path')).toBe(
78+
true
79+
);
80+
});
81+
82+
it('should handle complex safe patterns from existing tests', () => {
83+
// Jira pattern (safe version without .*? at the start)
84+
const jiraPattern =
85+
'furybee.atlassian.net\\/jira\\/software\\/c\\/projects\\/([a-zA-Z]{1,5})\\/boards\\/([0-9]{1,4})(\\?.*)?$';
86+
const jiraUrl =
87+
'https://furybee.atlassian.net/jira/software/c/projects/FB/boards/74?quickFilter=313';
88+
expect(_safeRegexTestSync(jiraPattern, jiraUrl)).toBe(true);
89+
90+
// Bible Gateway pattern
91+
const biblePattern =
92+
'https:\\/\\/www\\.biblegateway\\.com\\/passage\\/\\?search=.*version=(?!MOUNCE)(?!.*;).*';
93+
const bibleUrl = 'https://www.biblegateway.com/passage/?search=John+3&version=NASB';
94+
expect(_safeRegexTestSync(biblePattern, bibleUrl)).toBe(true);
95+
});
96+
97+
it('should block dangerous ReDoS patterns', () => {
98+
// These patterns are known to cause catastrophic backtracking
99+
const dangerousPatterns = [
100+
'(a+)+',
101+
'(a*)*',
102+
'(a|a)*',
103+
'(x+|x+y+)*',
104+
'(a+)+b',
105+
];
106+
107+
dangerousPatterns.forEach((pattern) => {
108+
const result = _safeRegexTestSync(pattern, 'aaaaaaaaaaaaaaaaaaaaaaaaa');
109+
expect(result).toBe(false);
110+
expect(console.warn).toHaveBeenCalledWith(
111+
expect.stringContaining('Unsafe regex pattern detected')
112+
);
113+
});
114+
});
115+
116+
it('should handle invalid regex patterns gracefully', () => {
117+
expect(_safeRegexTestSync('[', 'test')).toBe(false);
118+
expect(_safeRegexTestSync('((', 'test')).toBe(false);
119+
expect(console.warn).toHaveBeenCalled();
120+
});
121+
122+
it('should handle empty or invalid inputs', () => {
123+
expect(_safeRegexTestSync('', 'test')).toBe(false);
124+
expect(_safeRegexTestSync(null, 'test')).toBe(false);
125+
expect(_safeRegexTestSync(undefined, 'test')).toBe(false);
126+
});
127+
128+
it('should work with patterns from existing storage tests', () => {
129+
// These are the actual patterns used in storage.test.js
130+
expect(_safeRegexTestSync('example\\.com\\/path', 'https://example.com/path')).toBe(
131+
true
132+
);
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)