Skip to content

Commit 2948788

Browse files
committed
Keep the web scanner out of a store's endless link spaces
Wapiti's crawler was spending its whole four hour budget on one site because an online store builds a fresh URL for the same product on every page it appears on: the add to cart link carries an encoded copy of the page you came from, the review form has an address per product, and the login link carries a referer the same way. A populated catalog turns that into thousands of near identical links the crawler never reaches the end of, so it ran to its hard limit while the same scan on a site without the catalog loaded finished in minutes. The engine now tells wapiti to skip those spaces, and the add to cart, review post, login referer, logout, section load, wishlist and product compare paths, by passing each as an excluded address rooted at the target. A discovered path that lands in one of them is also dropped before it is handed back as a place to start crawling. None of these is real attack surface and every ordinary page is still crawled, so coverage is unchanged while a big store now finishes in the same minutes as a small one. A site that is not a store serves none of these paths, so the exclusions match nothing and are safe to send every time.
1 parent ca34633 commit 2948788

1 file changed

Lines changed: 50 additions & 2 deletions

File tree

‎app/core/dast_engines.py‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,24 @@ def command(tool: str, url: str, host: str, profile: str, workdir: Path,
263263
"--max-scan-time", str(max(600, hard - 300)), "-A", _ua(),
264264
"--store-session", str(workdir / "wapiti-session"),
265265
"--store-config", str(workdir / "wapiti-config")]
266+
# Keep the crawler out of the endless, low value URL spaces an online
267+
# store generates, so it spends its time on real pages instead.
268+
excludes = _crawl_excludes(url)
269+
for bad in excludes:
270+
argv += ["-x", bad]
266271
# Routes from the code and paths found by ffuf, as extra places to
267-
# start crawling from. The crawler cannot reach an unlinked route.
268-
for extra in _same_site(url, paths)[:60]:
272+
# start crawling from. The crawler cannot reach an unlinked route. A
273+
# discovered path that is itself one of the excluded traps is dropped,
274+
# so it is never handed back in as a starting point.
275+
trap_prefixes = [bad[:-1] for bad in excludes] # drop the trailing '*'
276+
seeded = 0
277+
for extra in _same_site(url, paths):
278+
if any(extra.startswith(px) for px in trap_prefixes):
279+
continue
269280
argv += ["-s", extra]
281+
seeded += 1
282+
if seeded >= 60:
283+
break
270284
return argv, report
271285
if tool == "sqlmap":
272286
out = workdir / "sqlmap"
@@ -310,6 +324,40 @@ def _same_site(url: str, paths) -> list:
310324
return out
311325

312326

327+
# URL spaces an online store, Magento above all, builds one of per product, so
328+
# a crawler never reaches the end of them, together with the ones that throw
329+
# the scanner's own session away. None is real attack surface: the add to cart
330+
# and login "referer" links only re encode the page you came from, so the same
331+
# product turns into a different URL on every page it is listed on; the review
332+
# post address needs a signed in session; and logout just ends the session the
333+
# scan is using. Left in, wapiti spends its whole budget walking thousands of
334+
# near identical links. Measured against a populated Magento catalog that was
335+
# four hours, cut to minutes once these were excluded, while every real page is
336+
# still crawled. A site that is not Magento serves none of these paths, so the
337+
# patterns match nothing and are safe to pass every time. Each is a full
338+
# address ending in a wildcard, because wapiti matches an exclude against the
339+
# whole URL from start to end.
340+
_CRAWL_TRAPS = (
341+
"/checkout/cart/add/*",
342+
"/checkout/cart/delete/*",
343+
"/review/product/post/*",
344+
"/customer/account/login/referer/*",
345+
"/customer/account/logout*",
346+
"/customer/section/load*",
347+
"/wishlist/*",
348+
"/catalog/product_compare/*",
349+
)
350+
351+
352+
def _crawl_excludes(url: str) -> list:
353+
"""Absolute wildcard URLs for the crawl traps above, rooted at the target."""
354+
from urllib.parse import urlparse
355+
356+
p = urlparse(url)
357+
base = f"{p.scheme}://{p.netloc}"
358+
return [base + tail for tail in _CRAWL_TRAPS]
359+
360+
313361
def links_in(url: str, body: str) -> list:
314362
"""Same site links in a page, the ones with a query string first."""
315363
from urllib.parse import urljoin

0 commit comments

Comments
 (0)