Skip to content

Commit 6d9e806

Browse files
committed
fix(security): resolve exponential ReDoS in globmatch via memoization (fixes #241)
1 parent bef1283 commit 6d9e806

2 files changed

Lines changed: 23 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1010
- `pw.io.postgres.write` now streams each batch into PostgreSQL through the binary `COPY` protocol instead of issuing one `INSERT` per row, giving a large throughput improvement (up to ~100x) on bulk writes. Both output modes use it: stream-of-changes copies straight into the target, while snapshot mode stages each batch in a temporary table and merges it with a single set-based upsert/delete.
1111

1212
### Fixed
13+
- Fixed an exponential ReDoS (Algorithmic Complexity) vulnerability in `_globmatch` by introducing memoization, reducing time complexity from $O(2^k)$ to $O(N \times M)$ when evaluating unauthenticated `filepath_globpattern` filters.
1314
- `pw.io.milvus.write` no longer intermittently fails with a "server unavailable" / "connect failed" error when pointed at a local `.db` file. The embedded local Milvus server reports itself as started before it actually accepts connections, so under load the first connection could lose the race against the server coming up; the connector now retries the initial connection until the local server is ready.
1415
- Improved concurrent write handling in pw.io.sqlite.write for SQLite databases. Writes to the same database file now produce deterministic output in multi-worker and multi-table setups.
1516
- `pw.io.elasticsearch.write` no longer fails when a minibatch is big enough that its Elasticsearch `_bulk` request would exceed a server-side limit. The connector reads both the cluster's `http.max_content_length` (the `413 Request Entity Too Large` limit) and `indexing_pressure.memory.limit` (the `429 Too Many Requests` limit, which on a small-heap node trips well below 100 MB) at start-up, and splits the buffered documents across as many bulk requests as needed to stay under whichever is hit first — so large batches are still written in as few requests as possible instead of being rejected. (Both limits fall back to a conservative default if they cannot be read.)

python/pathway/stdlib/ml/classifiers/_knn_lsh.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from typing import Literal
3131

3232
import jmespath
33+
import jmespath.exceptions
3334
import jmespath.functions
3435
import numpy as np
3536

@@ -98,18 +99,30 @@ def knn_lsh_classifier_train(
9899

99100

100101
# support for glob metadata search
101-
def _globmatch_impl(pat_i, pat_n, pattern, p_i, p_n, path):
102-
"""Match pattern to path, recursively expanding **."""
102+
def _globmatch_impl(pat_i, pat_n, pattern, p_i, p_n, path, memo):
103+
"""Match pattern to path, recursively expanding **, using memoization."""
104+
state = (pat_i, p_i)
105+
if state in memo:
106+
return memo[state]
107+
103108
if pat_i == pat_n:
104-
return p_i == p_n
109+
memo[state] = p_i == p_n
110+
return memo[state]
105111
if p_i == p_n:
106-
return False
112+
memo[state] = False
113+
return memo[state]
107114
if pattern[pat_i] == "**":
108-
return _globmatch_impl(
109-
pat_i, pat_n, pattern, p_i + 1, p_n, path
110-
) or _globmatch_impl(pat_i + 1, pat_n, pattern, p_i, p_n, path)
115+
res = _globmatch_impl(
116+
pat_i, pat_n, pattern, p_i + 1, p_n, path, memo
117+
) or _globmatch_impl(pat_i + 1, pat_n, pattern, p_i, p_n, path, memo)
118+
memo[state] = res
119+
return res
111120
if fnmatch.fnmatch(path[p_i], pattern[pat_i]):
112-
return _globmatch_impl(pat_i + 1, pat_n, pattern, p_i + 1, p_n, path)
121+
res = _globmatch_impl(pat_i + 1, pat_n, pattern, p_i + 1, p_n, path, memo)
122+
memo[state] = res
123+
return res
124+
125+
memo[state] = False
113126
return False
114127

115128

@@ -118,7 +131,7 @@ def _globmatch(pattern, path):
118131
pattern_parts = pattern.split("/")
119132
path_parts = path.split("/")
120133
return _globmatch_impl(
121-
0, len(pattern_parts), pattern_parts, 0, len(path_parts), path_parts
134+
0, len(pattern_parts), pattern_parts, 0, len(path_parts), path_parts, {}
122135
)
123136

124137

0 commit comments

Comments
 (0)