You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 33202f1
Browse filesBrowse the repository at this point in the historyBrowse files
Turn what outside AI pen testers proved into checks that cost nothing to run
Five vendors tested a PHP application. The weaknesses they proved now have
code rules: Dompdf running PHP inside rendered documents, the request path
written into a page unescaped, a formula guard that only looks at the first
character, uploads accepted on the browser's content type, address checks
that miss the 100.64.0.0/10 shared range, and forwarding headers trusted
from anywhere. Each was run against about twelve thousand files of other
code and reported only real problems.
New web checks find a dependency lock file served from the web root and
extra path after a PHP script written into the page, both confirmed against
real Apache and PHP. The open redirect check adds the path form and stops
reporting a redirect to the site's own sign in page, using a new
header_matches test in the rule language. Code reasoning names the access
mistakes those testers found most, without asking again about code it has
already read.
"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\")",
32
+
"differential": "status_in(301, 302, 303, 307, 308) and header_matches(\"location\", \"^\\\\s*(https?:)?[/\\\\\\\\]{2}canary-openredirect\\\\.example\")",
31
33
"retest": 2
32
34
},
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.",
35
+
"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 path form also works on some servers: curl -sk -i 'https://host/%5Ccanary-openredirect.example/%2f..' answering with Location: /\\canary-openredirect.example/..., which a browser reads as the outside host. A redirect to an outside host means the parameter is an open redirect.",
34
36
"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.",
"title": "Extra path after a PHP script is written into the page unescaped",
7
+
"summary": "A PHP page accepts extra path after its own name (page.php/anything) and writes it back into the HTML without encoding it, usually through $_SERVER['PHP_SELF'] in a form action or a link. A link with markup in that extra path puts the markup on the page: a meta refresh that sends the visitor to any site, a fake sign in form, or script where the content security policy allows it.",
8
+
"needs": [],
9
+
"candidate_paths": [
10
+
"/index.php/gcrpi%22%3E%3Cgcrpi7q%3E",
11
+
"/login.php/gcrpi%22%3E%3Cgcrpi7q%3E",
12
+
"/admin.php/gcrpi%22%3E%3Cgcrpi7q%3E",
13
+
"/search.php/gcrpi%22%3E%3Cgcrpi7q%3E",
14
+
"/contact.php/gcrpi%22%3E%3Cgcrpi7q%3E",
15
+
"/register.php/gcrpi%22%3E%3Cgcrpi7q%3E",
16
+
"/profile.php/gcrpi%22%3E%3Cgcrpi7q%3E",
17
+
"/dashboard.php/gcrpi%22%3E%3Cgcrpi7q%3E",
18
+
"/forgot-password.php/gcrpi%22%3E%3Cgcrpi7q%3E",
19
+
"/reset-password.php/gcrpi%22%3E%3Cgcrpi7q%3E"
20
+
],
21
+
"probe": [
22
+
{
23
+
"request": {
24
+
"method": "GET",
25
+
"path": "{route}",
26
+
"follow": false
27
+
}
28
+
}
29
+
],
30
+
"confirm": {
31
+
"control": "A path that cannot exist. The marker only ever goes in the probe, so a page that shows it unescaped wrote the request's own path into its markup.",
32
+
"differential": "status_in(200) and body_contains('\"><gcrpi7q>') and 'html' in header('content-type')",
33
+
"retest": 2
34
+
},
35
+
"reproduce": "curl -sk \"https://host/index.php/x%22%3E%3Cb%3Etest%3C/b%3E\" and look for the literal text \"><b>test</b> in the page, unescaped (not "><b>). Try the other script names the rule lists, and any other PHP page on the site. In a browser, a meta refresh in the same place sends the visitor to another site.",
36
+
"remediate": "Encode the value before writing it into HTML: htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8'). Better, do not use PHP_SELF or REQUEST_URI in markup at all: leave a form's action empty to post back to the same page, or write the script's fixed path. Turning off AcceptPathInfo in Apache also stops the extra path from reaching the script.",
"title": "Dependency manifest or lock file served from the web root",
8
+
"summary": "The site serves its composer, npm, yarn, Ruby or Python lock file to anybody. It lists every library and its exact version, which tells an attacker which known vulnerabilities to try first, and it shows that the project folder itself is the web root, so other files beside it may be served too.",
9
+
"needs": [],
10
+
"candidate_paths": [
11
+
"/composer.lock",
12
+
"/composer.json",
13
+
"/package-lock.json",
14
+
"/yarn.lock",
15
+
"/Gemfile.lock",
16
+
"/Pipfile.lock",
17
+
"/poetry.lock"
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 a site that answers every path with the same page is not mistaken for one serving the file.",
30
+
"differential": "status_in(200) and body_differs(control) and body_matches(\"(?m)\\\"content-hash\\\"\\\\s*:\\\\s*\\\"[0-9a-f]{32}\\\"|\\\"lockfileVersion\\\"\\\\s*:\\\\s*\\\\d|^# yarn lockfile v1|^GEM\\\\s*$\\\\s+remote:|\\\"_meta\\\"\\\\s*:\\\\s*\\\\{\\\\s*\\\"hash\\\"|^# This file is automatically @generated by Poetry|\\\"require\\\"\\\\s*:\\\\s*\\\\{[^}]*\\\"php\\\"\\\\s*:\")",
31
+
"retest": 2
32
+
},
33
+
"reproduce": "curl -sk 'https://host/composer.lock' | head -20 and look for \"content-hash\" and a \"packages\" list. Try package-lock.json (\"lockfileVersion\"), yarn.lock (\"# yarn lockfile v1\"), Gemfile.lock (\"GEM\" then \"remote:\"), Pipfile.lock (\"_meta\") and poetry.lock. Compare with a path that cannot exist, so a page served for every path is not mistaken for the file.",
34
+
"remediate": "Serve the application from a public folder that holds only what browsers need, not from the project root. If that cannot change yet, deny dependency and lock files in the web server (for Apache: <FilesMatch \"^(composer\\.(json|lock)|package(-lock)?\\.json|yarn\\.lock|Gemfile\\.lock|Pipfile\\.lock|poetry\\.lock)$\"> Require all denied </FilesMatch>).",
0 commit comments