Skip to content

Commit 10a476b

Browse files
committed
Add business logic and client side curated rules, and a probe that does not follow redirects
Three new read only curated rules, each confirmed against a control, for the two rule classes that had none. Client side: a site that reflects any Origin into Access-Control-Allow-Origin while allowing credentials, so any other website a signed-in user visits can read their data from this one. Business logic: an open redirect, a redirect parameter that sends a visitor to any outside site, which is how a phishing link is made to look like it belongs here and how an OAuth token is stolen; and a Host header the site trusts, using the attacker's value to build links or redirects, which poisons a password reset email. To support the redirect checks, a probe can now ask not to follow redirects, so a rule can read the Location header a redirect carries without being bounced to the place it points. The real world precedent table gained these three classes so the review can ground them.
1 parent 04c6795 commit 10a476b

5 files changed

Lines changed: 149 additions & 3 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"id": "business-logic-host-header-injection",
3+
"class": "business-logic",
4+
"safety": "read",
5+
"severity_floor": "medium",
6+
"title": "The site trusts an attacker's Host header",
7+
"summary": "The site takes the Host header from the request and uses it to build links or redirects, instead of its own configured name. An attacker who sets the Host can poison a password reset email so its link points at their site, or poison a shared cache. The check sends a made-up host and sees it come back.",
8+
"needs": [],
9+
"probe": [
10+
{
11+
"request": {
12+
"method": "GET",
13+
"path": "/",
14+
"headers": {
15+
"Host": "canary-host-injection.example"
16+
},
17+
"follow": false
18+
}
19+
}
20+
],
21+
"confirm": {
22+
"control": "A normal request with the site's own Host, so the made-up host coming back is clearly a reflection of the attacker's value.",
23+
"differential": "reflected(\"canary-host-injection.example\") or \"canary-host-injection.example\" in header(\"location\")",
24+
"retest": 2
25+
},
26+
"reproduce": "curl -sk -i 'https://host/' -H 'Host: canary-host-injection.example' and check whether canary-host-injection.example comes back in the response body or in a Location header. If it does, the site is building links or redirects from the attacker-controlled Host, which enables password reset poisoning.",
27+
"remediate": "Build absolute links and redirects from a fixed, configured host name, not from the Host header. Reject requests whose Host is not an expected value at the web tier.",
28+
"references": [],
29+
"mappings": {
30+
"cwe": "CWE-644",
31+
"owasp": "A03:2021"
32+
},
33+
"rulesets": []
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"id": "business-logic-open-redirect",
3+
"class": "business-logic",
4+
"safety": "read",
5+
"severity_floor": "medium",
6+
"title": "Open redirect sends visitors to any site",
7+
"summary": "A redirect parameter sends the visitor wherever its value points, including an outside site. Attackers use that to make a phishing link look like it belongs to this company, and to steal OAuth tokens by redirecting the sign-in response to a site they control.",
8+
"needs": [],
9+
"candidate_paths": [
10+
"/?next=https://canary-openredirect.example/",
11+
"/?url=https://canary-openredirect.example/",
12+
"/?redirect=https://canary-openredirect.example/",
13+
"/?return=https://canary-openredirect.example/",
14+
"/?returnUrl=https://canary-openredirect.example/",
15+
"/?dest=https://canary-openredirect.example/",
16+
"/login?next=https://canary-openredirect.example/",
17+
"/logout?next=https://canary-openredirect.example/"
18+
],
19+
"probe": [
20+
{
21+
"request": {
22+
"method": "GET",
23+
"path": "{route}",
24+
"follow": false
25+
}
26+
}
27+
],
28+
"confirm": {
29+
"control": "A path that cannot exist, so the site's not found answer is known and a real redirect to the foreign host is told apart from it.",
30+
"differential": "status_in(301, 302, 303, 307, 308) and \"canary-openredirect.example\" in header(\"location\")",
31+
"retest": 2
32+
},
33+
"reproduce": "curl -sk -i 'https://host/?next=https://canary-openredirect.example/' and check the response is a 30x whose Location header points at canary-openredirect.example. Try the other parameter names (url, redirect, return, returnUrl, dest) and the login and logout paths. A redirect to an outside host means the parameter is an open redirect.",
34+
"remediate": "Do not redirect to a location taken from the request. Allow only relative paths, or an explicit allow list of hosts, and reject or ignore an absolute URL to another site.",
35+
"references": [],
36+
"mappings": {
37+
"cwe": "CWE-601",
38+
"owasp": "A01:2021"
39+
},
40+
"rulesets": []
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"id": "client-side-cors-any-origin-credentials",
3+
"class": "client-side",
4+
"safety": "read",
5+
"severity_floor": "high",
6+
"title": "CORS reflects any origin and allows credentials",
7+
"summary": "The site echoes whatever Origin a request carries into Access-Control-Allow-Origin and sets Access-Control-Allow-Credentials to true. That lets any other website make credentialed cross-origin requests and read the response, so a malicious page a signed-in user visits can read their data from this site.",
8+
"needs": [],
9+
"probe": [
10+
{
11+
"request": {
12+
"method": "GET",
13+
"path": "/",
14+
"headers": {
15+
"Origin": "https://canary-cors.example"
16+
}
17+
}
18+
}
19+
],
20+
"confirm": {
21+
"control": "A normal request without the foreign Origin, so the reflected origin stands out as a reflection, not the site's own policy.",
22+
"differential": "header(\"access-control-allow-origin\") == \"https://canary-cors.example\" and header(\"access-control-allow-credentials\") == \"true\"",
23+
"retest": 2
24+
},
25+
"reproduce": "curl -sk -i 'https://host/' -H 'Origin: https://canary-cors.example' and check the response: Access-Control-Allow-Origin comes back as https://canary-cors.example and Access-Control-Allow-Credentials is true. Reflecting an arbitrary origin with credentials means any site can read a signed-in user's data cross-origin.",
26+
"remediate": "Do not reflect the Origin header. Allow credentials only for an explicit allow list of trusted origins, and never combine Access-Control-Allow-Credentials: true with an origin the request supplied or with a wildcard.",
27+
"references": [],
28+
"mappings": {
29+
"cwe": "CWE-942",
30+
"owasp": "A05:2021"
31+
},
32+
"rulesets": []
33+
}

‎app/core/dast_checks/engine.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def _allowed_method(self, method: str) -> bool:
8181
return method in spec_mod.ALL_METHODS
8282

8383
def request(self, method: str, path: str, query: dict = None,
84-
headers: dict = None, body: str = None) -> dict:
84+
headers: dict = None, body: str = None, follow: bool = True) -> dict:
8585
from app.core import external_probe
8686

8787
method = (method or "GET").upper()
@@ -100,7 +100,8 @@ def request(self, method: str, path: str, query: dict = None,
100100
sent.update(headers or {})
101101
started = time.monotonic()
102102
got = external_probe.fetch(url, method=method, headers=sent,
103-
allow_private=self.allow_private, body=body)
103+
allow_private=self.allow_private, body=body,
104+
follow=follow)
104105
got["seconds"] = time.monotonic() - started
105106
got.setdefault("body", "")
106107
return got
@@ -916,7 +917,8 @@ def _run_probe(spec: dict, client: Client, route: str, oob_value: str) -> dict:
916917
query = {k: _expand(str(v), route, oob_value)
917918
for k, v in (req.get("query") or {}).items()}
918919
last = client.request(req.get("method") or "GET", path, query=query,
919-
headers=req.get("headers") or {})
920+
headers=req.get("headers") or {},
921+
follow=bool(req.get("follow", True)))
920922
return last
921923

922924

‎app/core/dast_precedent.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,42 @@
122122
"about 300,000 accounts by reusing passwords leaked from other "
123123
"sites.",
124124
},
125+
{
126+
"key": "cors",
127+
"match": re.compile(r"\bcors\b|cross.origin resource sharing|"
128+
r"access-control-allow-origin|CWE-?942\b", re.I),
129+
"ref": "CWE-942; OWASP A05:2021",
130+
"abuse": "A malicious site that a signed-in user visits makes a credentialed "
131+
"cross-origin request to this site and reads the authenticated "
132+
"response, taking the user's own data.",
133+
"precedent": "A permissive CORS policy that reflects any origin with "
134+
"credentials is a well documented cause of cross-origin data "
135+
"theft, recognised in OWASP guidance and reported in many "
136+
"public disclosures.",
137+
},
138+
{
139+
"key": "open-redirect",
140+
"match": re.compile(r"open redirect|unvalidated redirect|CWE-?601\b", re.I),
141+
"ref": "CWE-601; OWASP A01:2021",
142+
"abuse": "The attacker crafts a link on this trusted site that bounces the "
143+
"visitor to a site they control, for phishing or to capture an "
144+
"OAuth token from a sign-in response.",
145+
"precedent": "Open redirects are a standard phishing and OAuth token theft "
146+
"technique, documented in the OWASP Top 10 and used repeatedly "
147+
"against sign-in flows.",
148+
},
149+
{
150+
"key": "host-header-injection",
151+
"match": re.compile(r"host header|host injection|password reset poison|"
152+
r"cache poison|CWE-?644\b", re.I),
153+
"ref": "CWE-644",
154+
"abuse": "The attacker sets the Host header so the site builds a password "
155+
"reset link, or a cached page, that points at the attacker's "
156+
"server, then waits for a victim to use it.",
157+
"precedent": "Host header injection that poisons password reset emails and "
158+
"web caches is a documented technique, described by security "
159+
"researchers and in OWASP guidance.",
160+
},
125161
{
126162
"key": "data-exposure",
127163
"match": re.compile(r"directory listing|directory index|exposed backup|"

0 commit comments

Comments
 (0)