Skip to content

Commit

Permalink
Update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
afterxleep committed Feb 10, 2025
1 parent 8b6bf25 commit 60615f3
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 5 deletions.
89 changes: 84 additions & 5 deletions Bouncer/Models/SMSFilter/SMSOfflineFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,97 @@ struct SMSOfflineFilter {
return result
}

private func isUnsafeRegexPattern(_ pattern: String) -> Bool {
// Check for common dangerous patterns
let dangerousPatterns = [
// Nested quantifiers
".*.*", ".+.+", "(a+)+", "(a*)*", "(a?)?+", "((a+)?)+",
// Overlapping patterns with quantifiers
"(a|a)+", "(a|aa)+",
// Recursive patterns
"(?R)", "(?0)"
]

// Check if pattern contains any dangerous constructs
if dangerousPatterns.contains(where: { pattern.contains($0) }) {
return true
}

// Check for excessive quantifiers
let quantifierPattern = "\\{\\d+,?\\d*\\}"
if let regex = try? NSRegularExpression(pattern: quantifierPattern) {
let matches = regex.matches(in: pattern, range: NSRange(pattern.startIndex..., in: pattern))
for match in matches {
let range = Range(match.range, in: pattern)!
let quantifier = pattern[range]
// Convert the quantifier range to string and split by comma
let quantifierStr = String(quantifier)
let numbers = quantifierStr.dropFirst().dropLast().split(separator: ",")

// Parse the first number (minimum)
guard let firstNumber = Int(String(numbers[0])) else {
return true // Invalid number format
}

// If there's a second number (maximum), parse it
let secondNumber: Int?
if numbers.count > 1 {
secondNumber = Int(String(numbers[1]))
} else {
secondNumber = nil
}

// Check if either number exceeds our limit
if firstNumber > 10000 || (secondNumber ?? 0) > 10000 {
return true
}
}
}

return false
}

private func matchRegex(text: String, filter: Filter) -> Bool {
// Handle empty text or filter phrase
if text.isEmpty || filter.phrase.isEmpty {
return false
}

var matchOptions: String.CompareOptions = [.regularExpression]
if !filter.caseSensitive {
matchOptions.insert(.caseInsensitive)
// Validate regex pattern
if isUnsafeRegexPattern(filter.phrase) {
os_log("FILTEREXTENSION - Unsafe regex pattern detected: %@", log: OSLog.messageFilterLog, type: .error, filter.phrase)
return false
}

// Try creating the regex first to validate it
do {
_ = try NSRegularExpression(pattern: filter.phrase)
} catch {
os_log("FILTEREXTENSION - Invalid regex pattern: %@", log: OSLog.messageFilterLog, type: .error, filter.phrase)
return false
}

// Set a reasonable timeout for regex matching
let timeout = DispatchTime.now() + .milliseconds(100)
var result = false
let group = DispatchGroup()
group.enter()

DispatchQueue.global(qos: .userInitiated).async {
var matchOptions: String.CompareOptions = [.regularExpression]
if !filter.caseSensitive {
matchOptions.insert(.caseInsensitive)
}
result = (text.range(of: filter.phrase, options: matchOptions) != nil)
group.leave()
}

// Wait with timeout
if group.wait(timeout: timeout) == .timedOut {
os_log("FILTEREXTENSION - Regex matching timed out for pattern: %@", log: OSLog.messageFilterLog, type: .error, filter.phrase)
return false
}
// Text is already trimmed in applyFilter
let result = (text.range(of: filter.phrase, options: matchOptions) != nil)

os_log("FILTEREXTENSION - -- Match: %@", log: OSLog.messageFilterLog, type: .info, "\(result)")
os_log("FILTEREXTENSION - -- Method: Regex", log: OSLog.messageFilterLog, type: .info)
return result
Expand Down
Binary file modified BouncerTests/Models/AppSettingsDefaultsTests.swift.plist
Binary file not shown.
116 changes: 116 additions & 0 deletions BouncerTests/Models/SMSOfflineFilterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -713,4 +713,120 @@ class SMSOfflineFilterTests: XCTestCase {
XCTAssertEqual(result.action, .transaction)
XCTAssertEqual(result.subAction, .transactionalFinance)
}

// MARK: - Additional Edge Cases

func testRegexTimeoutAndSafety() {
// Test regex timeout with potentially problematic pattern
let message = SMSMessage(sender: "Service", text: String(repeating: "a", count: 1000))
let filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "(a+)+b", type: .message, action: .junk, useRegex: true)
])
// Should return false due to safety check or timeout
XCTAssertEqual(filter.filterMessage(message: message).action, .none)

// Test other potentially dangerous patterns
let dangerousPatterns = [
".*.*", ".+.+", "(a*)*", "(a?)?+", "((a+)?)+",
"(a|a)+", "(a|aa)+", "a{100000}"
]

for pattern in dangerousPatterns {
let filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: pattern, type: .message, action: .junk, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: message).action, .none,
"Dangerous pattern \(pattern) should be rejected")
}
}

func testMultilineAndSpecialCharacters() {
// Test multiline message
let multilineMessage = SMSMessage(sender: "Service", text: "Line 1\nLine 2\nLine 3")
var filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "Line [0-9]", type: .message, action: .transaction, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: multilineMessage).action, .transaction)

// Test message with special regex characters as literal text
let specialCharsMessage = SMSMessage(sender: "Service", text: "Price: $100.00 (50% off)")
filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "\\$\\d+\\.\\d+", type: .message, action: .promotion, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: specialCharsMessage).action, .promotion)
}

func testUnicodeAndBoundaryHandling() {
// Test Unicode category matching
let message1 = SMSMessage(sender: "Service", text: "Testing numbers 123 and symbols @#$")
var filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "\\d+", type: .message, action: .transaction, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: message1).action, .transaction)

// Test word boundaries with international characters
// Test word boundaries with international characters
let message2 = SMSMessage(sender: "Service", text: "my café here")
filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "\\bcafé\\b", type: .message, action: .promotion, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: message2).action, .promotion)

// Test zero-width characters
let message3 = SMSMessage(sender: "Service", text: "Hello\u{200B}World")
filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "Hello.*World", type: .message, action: .transaction, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: message3).action, .transaction)
}

func testConcurrentFilterProcessing() {
// Test multiple filters being processed concurrently
let message = SMSMessage(sender: "Bank-Alert", text: "Your OTP is 123456")

// Create a large number of filters to test concurrent processing
var filters: [Filter] = []
for i in 0..<100 {
filters.append(Filter(id: UUID(),
phrase: "\\d{6}",
type: .message,
action: i % 2 == 0 ? .transaction : .promotion,
useRegex: true))
}

let filter = SMSOfflineFilter(filterList: filters)
let result = filter.filterMessage(message: message)

// First matching filter should win regardless of concurrent processing
XCTAssertEqual(result.action, .transaction)
}

func testRegexOptimizationAndLimits() {
// Test regex pattern with excessive backtracking but within limits
let message = SMSMessage(sender: "Service", text: String(repeating: "a", count: 50))
var filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "a{1,50}b?", type: .message, action: .transaction, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: message).action, .transaction)

// Test pattern with reasonable repetition
filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "\\d{1,10}", type: .message, action: .transaction, useRegex: true)
])
XCTAssertEqual(filter.filterMessage(message: SMSMessage(sender: "Service", text: "123456")).action, .transaction)

// Test pattern with excessive repetition
filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "\\d{1,20000}", type: .message, action: .transaction, useRegex: true)
])
// Should be rejected due to excessive quantifier
XCTAssertEqual(filter.filterMessage(message: SMSMessage(sender: "Service", text: "123456")).action, .none)

// Test pattern with invalid regex syntax
filter = SMSOfflineFilter(filterList: [
Filter(id: UUID(), phrase: "[invalid", type: .message, action: .transaction, useRegex: true)
])
// Should be rejected due to invalid syntax
XCTAssertEqual(filter.filterMessage(message: SMSMessage(sender: "Service", text: "123456")).action, .none)
}
}

0 comments on commit 60615f3

Please sign in to comment.