hackable 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hackable-1.0.0/LICENSE +21 -0
- hackable-1.0.0/PKG-INFO +104 -0
- hackable-1.0.0/README.md +84 -0
- hackable-1.0.0/pyproject.toml +31 -0
- hackable-1.0.0/setup.cfg +4 -0
- hackable-1.0.0/src/hackable/__init__.py +3 -0
- hackable-1.0.0/src/hackable/__main__.py +4 -0
- hackable-1.0.0/src/hackable/checks/__init__.py +22 -0
- hackable-1.0.0/src/hackable/checks/cookies.py +65 -0
- hackable-1.0.0/src/hackable/checks/cors.py +68 -0
- hackable-1.0.0/src/hackable/checks/debug.py +43 -0
- hackable-1.0.0/src/hackable/checks/disclosure.py +36 -0
- hackable-1.0.0/src/hackable/checks/exposed_files.py +122 -0
- hackable-1.0.0/src/hackable/checks/headers.py +70 -0
- hackable-1.0.0/src/hackable/checks/methods.py +28 -0
- hackable-1.0.0/src/hackable/checks/open_redirect.py +57 -0
- hackable-1.0.0/src/hackable/checks/rate_limit.py +53 -0
- hackable-1.0.0/src/hackable/checks/robots.py +31 -0
- hackable-1.0.0/src/hackable/checks/securitytxt.py +21 -0
- hackable-1.0.0/src/hackable/checks/sqli.py +152 -0
- hackable-1.0.0/src/hackable/checks/tls.py +89 -0
- hackable-1.0.0/src/hackable/checks/xss.py +83 -0
- hackable-1.0.0/src/hackable/cli.py +117 -0
- hackable-1.0.0/src/hackable/crawler.py +98 -0
- hackable-1.0.0/src/hackable/findings.py +43 -0
- hackable-1.0.0/src/hackable/polite.py +47 -0
- hackable-1.0.0/src/hackable/report.py +244 -0
- hackable-1.0.0/src/hackable/scanner.py +60 -0
- hackable-1.0.0/src/hackable.egg-info/PKG-INFO +104 -0
- hackable-1.0.0/src/hackable.egg-info/SOURCES.txt +33 -0
- hackable-1.0.0/src/hackable.egg-info/dependency_links.txt +1 -0
- hackable-1.0.0/src/hackable.egg-info/entry_points.txt +2 -0
- hackable-1.0.0/src/hackable.egg-info/requires.txt +1 -0
- hackable-1.0.0/src/hackable.egg-info/top_level.txt +1 -0
- hackable-1.0.0/tests/test_checks.py +296 -0
hackable-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shafqat Mehmood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hackable-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hackable
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Hack yourself before they do. One-command website security check for people who have never run a security tool.
|
|
5
|
+
Author: Shafqat Mehmood
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Shifu34/hackable
|
|
8
|
+
Keywords: security,pentest,scanner,web,vulnerability,dast
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# hackable
|
|
22
|
+
|
|
23
|
+
**Hack yourself before they do.**
|
|
24
|
+
|
|
25
|
+
One-command website security check for people who have never run a security tool. Point it at your app, and in about ten seconds it tells you, in plain English, how a hacker would break in and exactly how to stop them.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
$ pip install hackable
|
|
29
|
+
$ hackable https://myapp.com
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## The problem
|
|
35
|
+
|
|
36
|
+
Millions of apps are being built with AI right now by people who have never heard of OWASP. The apps work. They also leak database keys, take SQL injection, and let anyone log in as anyone. The existing scanners (ZAP, Nuclei, Burp) are expert tools with expert UX. They might as well be in another language.
|
|
37
|
+
|
|
38
|
+
hackable is the Let's Encrypt of pentesting: free, one command, plain English.
|
|
39
|
+
|
|
40
|
+
## What it checks
|
|
41
|
+
|
|
42
|
+
14 checks. Before probing, hackable crawls your app like a visitor would,
|
|
43
|
+
collecting real links and forms, so injection tests hit your actual inputs
|
|
44
|
+
instead of guessed parameter names.
|
|
45
|
+
|
|
46
|
+
| Check | What it finds |
|
|
47
|
+
|---|---|
|
|
48
|
+
| SQL injection | Error-based probes plus boolean differential checks (safe, GET-only) |
|
|
49
|
+
| Exposed files | Public `/.env`, `/.git/`, config backups, with content verification (no false alarms from SPA fallbacks) |
|
|
50
|
+
| XSS | Your input reflected unescaped into the page |
|
|
51
|
+
| Open redirects | Your site redirecting visitors to attacker domains |
|
|
52
|
+
| CORS | Arbitrary origins trusted with credentials |
|
|
53
|
+
| Debug mode | Stack traces leaking from error pages |
|
|
54
|
+
| Login rate limiting | 10 rapid wrong passwords, checking for pushback |
|
|
55
|
+
| Security headers | HSTS, CSP, X-Frame-Options, and friends |
|
|
56
|
+
| Cookie flags | Session cookies missing Secure, HttpOnly, or SameSite |
|
|
57
|
+
| TLS | Missing HTTPS, expired certs, ancient TLS versions |
|
|
58
|
+
| Version disclosure | Server headers announcing exact versions |
|
|
59
|
+
| HTTP methods | Risky methods like TRACE |
|
|
60
|
+
| robots.txt | Hidden paths handed to attackers on a plate |
|
|
61
|
+
| security.txt | No standard contact for researchers to report issues |
|
|
62
|
+
|
|
63
|
+
Every finding comes with **what this means** (one sentence, no jargon) and **how to fix it** (one sentence, actionable). You get a 0-100 score and an A-F grade.
|
|
64
|
+
|
|
65
|
+
## Output
|
|
66
|
+
|
|
67
|
+
Terminal report, machine-readable JSON (`--json`), SARIF 2.1.0 for GitHub code
|
|
68
|
+
scanning (`--sarif`), and a self-contained shareable HTML report card
|
|
69
|
+
(`--html report.html`).
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
hackable https://myapp.com --html report.html
|
|
73
|
+
hackable https://myapp.com --fail-under 70 # exit 1 in CI if score < 70
|
|
74
|
+
hackable https://myapp.com --only sqli,xss # run a subset of checks
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## GitHub Action
|
|
78
|
+
|
|
79
|
+
Drop it into your workflow and every push gets scanned:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
- uses: Shifu34/hackable@v1
|
|
83
|
+
with:
|
|
84
|
+
target: https://myapp.com
|
|
85
|
+
fail-under: 70
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Results upload to the Security tab automatically.
|
|
89
|
+
|
|
90
|
+
## Safety
|
|
91
|
+
|
|
92
|
+
hackable only sends harmless probes: no destructive payloads, no data deletion, no login attempts with real credentials, ~50-150 requests with delays and an identifying User-Agent. It will ask for confirmation before scanning unless you pass `--yes`.
|
|
93
|
+
|
|
94
|
+
**Only scan apps you own or have explicit permission to test.** Scanning without permission may be illegal.
|
|
95
|
+
|
|
96
|
+
## Roadmap
|
|
97
|
+
|
|
98
|
+
- Authenticated scans (test behind a login)
|
|
99
|
+
- JavaScript-rendered app support
|
|
100
|
+
- Scheduled scans with diff alerts
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
hackable-1.0.0/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# hackable
|
|
2
|
+
|
|
3
|
+
**Hack yourself before they do.**
|
|
4
|
+
|
|
5
|
+
One-command website security check for people who have never run a security tool. Point it at your app, and in about ten seconds it tells you, in plain English, how a hacker would break in and exactly how to stop them.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
$ pip install hackable
|
|
9
|
+
$ hackable https://myapp.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## The problem
|
|
15
|
+
|
|
16
|
+
Millions of apps are being built with AI right now by people who have never heard of OWASP. The apps work. They also leak database keys, take SQL injection, and let anyone log in as anyone. The existing scanners (ZAP, Nuclei, Burp) are expert tools with expert UX. They might as well be in another language.
|
|
17
|
+
|
|
18
|
+
hackable is the Let's Encrypt of pentesting: free, one command, plain English.
|
|
19
|
+
|
|
20
|
+
## What it checks
|
|
21
|
+
|
|
22
|
+
14 checks. Before probing, hackable crawls your app like a visitor would,
|
|
23
|
+
collecting real links and forms, so injection tests hit your actual inputs
|
|
24
|
+
instead of guessed parameter names.
|
|
25
|
+
|
|
26
|
+
| Check | What it finds |
|
|
27
|
+
|---|---|
|
|
28
|
+
| SQL injection | Error-based probes plus boolean differential checks (safe, GET-only) |
|
|
29
|
+
| Exposed files | Public `/.env`, `/.git/`, config backups, with content verification (no false alarms from SPA fallbacks) |
|
|
30
|
+
| XSS | Your input reflected unescaped into the page |
|
|
31
|
+
| Open redirects | Your site redirecting visitors to attacker domains |
|
|
32
|
+
| CORS | Arbitrary origins trusted with credentials |
|
|
33
|
+
| Debug mode | Stack traces leaking from error pages |
|
|
34
|
+
| Login rate limiting | 10 rapid wrong passwords, checking for pushback |
|
|
35
|
+
| Security headers | HSTS, CSP, X-Frame-Options, and friends |
|
|
36
|
+
| Cookie flags | Session cookies missing Secure, HttpOnly, or SameSite |
|
|
37
|
+
| TLS | Missing HTTPS, expired certs, ancient TLS versions |
|
|
38
|
+
| Version disclosure | Server headers announcing exact versions |
|
|
39
|
+
| HTTP methods | Risky methods like TRACE |
|
|
40
|
+
| robots.txt | Hidden paths handed to attackers on a plate |
|
|
41
|
+
| security.txt | No standard contact for researchers to report issues |
|
|
42
|
+
|
|
43
|
+
Every finding comes with **what this means** (one sentence, no jargon) and **how to fix it** (one sentence, actionable). You get a 0-100 score and an A-F grade.
|
|
44
|
+
|
|
45
|
+
## Output
|
|
46
|
+
|
|
47
|
+
Terminal report, machine-readable JSON (`--json`), SARIF 2.1.0 for GitHub code
|
|
48
|
+
scanning (`--sarif`), and a self-contained shareable HTML report card
|
|
49
|
+
(`--html report.html`).
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
hackable https://myapp.com --html report.html
|
|
53
|
+
hackable https://myapp.com --fail-under 70 # exit 1 in CI if score < 70
|
|
54
|
+
hackable https://myapp.com --only sqli,xss # run a subset of checks
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## GitHub Action
|
|
58
|
+
|
|
59
|
+
Drop it into your workflow and every push gets scanned:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- uses: Shifu34/hackable@v1
|
|
63
|
+
with:
|
|
64
|
+
target: https://myapp.com
|
|
65
|
+
fail-under: 70
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Results upload to the Security tab automatically.
|
|
69
|
+
|
|
70
|
+
## Safety
|
|
71
|
+
|
|
72
|
+
hackable only sends harmless probes: no destructive payloads, no data deletion, no login attempts with real credentials, ~50-150 requests with delays and an identifying User-Agent. It will ask for confirmation before scanning unless you pass `--yes`.
|
|
73
|
+
|
|
74
|
+
**Only scan apps you own or have explicit permission to test.** Scanning without permission may be illegal.
|
|
75
|
+
|
|
76
|
+
## Roadmap
|
|
77
|
+
|
|
78
|
+
- Authenticated scans (test behind a login)
|
|
79
|
+
- JavaScript-rendered app support
|
|
80
|
+
- Scheduled scans with diff alerts
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hackable"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Hack yourself before they do. One-command website security check for people who have never run a security tool."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Shafqat Mehmood" }]
|
|
13
|
+
keywords = ["security", "pentest", "scanner", "web", "vulnerability", "dast"]
|
|
14
|
+
dependencies = ["requests"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/Shifu34/hackable"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
hackable = "hackable.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
hackable-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Security checks. Each exposes run(base, http) -> list[Finding].
|
|
2
|
+
Injection checks (sqli, xss, open_redirect) also accept an optional third
|
|
3
|
+
argument: discovered (url, [params]) pairs from the crawler."""
|
|
4
|
+
|
|
5
|
+
from . import cookies, cors, debug, disclosure, exposed_files, headers, methods, open_redirect, rate_limit, robots, securitytxt, sqli, tls, xss
|
|
6
|
+
|
|
7
|
+
CHECKS = [
|
|
8
|
+
("headers", "security headers", headers.run),
|
|
9
|
+
("robots", "robots.txt", robots.run),
|
|
10
|
+
("disclosure", "version disclosure", disclosure.run),
|
|
11
|
+
("exposed_files", "exposed sensitive files", exposed_files.run),
|
|
12
|
+
("debug", "debug mode / stack traces", debug.run),
|
|
13
|
+
("cors", "CORS misconfiguration", cors.run),
|
|
14
|
+
("methods", "dangerous HTTP methods", methods.run),
|
|
15
|
+
("cookies", "cookie security flags", cookies.run),
|
|
16
|
+
("sqli", "SQL injection", sqli.run),
|
|
17
|
+
("xss", "cross-site scripting", xss.run),
|
|
18
|
+
("open_redirect", "open redirects", open_redirect.run),
|
|
19
|
+
("rate_limit", "login rate limiting", rate_limit.run),
|
|
20
|
+
("tls", "TLS / HTTPS", tls.run),
|
|
21
|
+
("securitytxt", "security.txt", securitytxt.run),
|
|
22
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Check that cookies, especially session cookies, carry security flags."""
|
|
2
|
+
|
|
3
|
+
from ..findings import Finding
|
|
4
|
+
|
|
5
|
+
FLAG_ADVICE = {
|
|
6
|
+
"secure": ("medium", "Cookie without the Secure flag",
|
|
7
|
+
"This cookie can be sent over plain HTTP, where anyone on the "
|
|
8
|
+
"network can read or steal it.",
|
|
9
|
+
"Fix: set the Secure attribute so the cookie is only ever "
|
|
10
|
+
"sent over HTTPS."),
|
|
11
|
+
"httponly": ("low", "Cookie without the HttpOnly flag",
|
|
12
|
+
"JavaScript running in the page can read this cookie, which "
|
|
13
|
+
"makes session theft via XSS much easier.",
|
|
14
|
+
"Fix: set the HttpOnly attribute so JavaScript cannot read "
|
|
15
|
+
"the cookie. This blunts XSS-based session theft."),
|
|
16
|
+
"samesite": ("low", "Cookie without the SameSite attribute",
|
|
17
|
+
"The browser sends this cookie on cross-site requests, which "
|
|
18
|
+
"enables cross-site request forgery attacks.",
|
|
19
|
+
"Fix: set SameSite=Lax (or Strict) to block the cookie on "
|
|
20
|
+
"cross-site requests and reduce CSRF risk."),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _raw_cookies(response):
|
|
25
|
+
try:
|
|
26
|
+
return response.raw.headers.getlist("set-cookie")
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
single = response.headers.get("set-cookie")
|
|
30
|
+
return [single] if single else []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(base, http):
|
|
34
|
+
r = http.get(base + "/")
|
|
35
|
+
if r is None:
|
|
36
|
+
return []
|
|
37
|
+
raw = _raw_cookies(r)
|
|
38
|
+
if not raw:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
is_https = base.startswith("https://")
|
|
42
|
+
missing = {"secure": [], "httponly": [], "samesite": []}
|
|
43
|
+
for cookie in raw:
|
|
44
|
+
parts = [p.strip() for p in cookie.split(";")]
|
|
45
|
+
name = parts[0].split("=", 1)[0] or "(unnamed)"
|
|
46
|
+
flags = {p.split("=", 1)[0].lower() for p in parts[1:]}
|
|
47
|
+
for flag in missing:
|
|
48
|
+
if flag == "secure" and not is_https:
|
|
49
|
+
continue # Secure only makes sense over HTTPS
|
|
50
|
+
if flag not in flags:
|
|
51
|
+
missing[flag].append(name)
|
|
52
|
+
|
|
53
|
+
findings = []
|
|
54
|
+
for flag, names in missing.items():
|
|
55
|
+
if not names:
|
|
56
|
+
continue
|
|
57
|
+
severity, title, meaning, fix = FLAG_ADVICE[flag]
|
|
58
|
+
findings.append(Finding(
|
|
59
|
+
check="cookies", severity=severity,
|
|
60
|
+
title=f"{title}: {', '.join(names[:3])}",
|
|
61
|
+
meaning=meaning,
|
|
62
|
+
url=base + "/", evidence=f"Set-Cookie flags seen: {flag} missing",
|
|
63
|
+
fix=fix,
|
|
64
|
+
))
|
|
65
|
+
return findings
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""CORS misconfiguration."""
|
|
2
|
+
|
|
3
|
+
from ..findings import Finding
|
|
4
|
+
|
|
5
|
+
EVIL = "https://evil-hackable-test.example.com"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _check_response(r):
|
|
9
|
+
acao = r.headers.get("Access-Control-Allow-Origin", "")
|
|
10
|
+
acac = r.headers.get("Access-Control-Allow-Credentials", "").lower() == "true"
|
|
11
|
+
if acao == EVIL and acac:
|
|
12
|
+
return "critical"
|
|
13
|
+
if acao == EVIL or (acao == "*" and acac):
|
|
14
|
+
return "high"
|
|
15
|
+
if acao == "*":
|
|
16
|
+
return "info"
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(base, http):
|
|
21
|
+
r = http.options(base + "/", headers={"Origin": EVIL})
|
|
22
|
+
verdict = _check_response(r) if r is not None else None
|
|
23
|
+
if verdict is None:
|
|
24
|
+
r = http.get(base + "/", headers={"Origin": EVIL})
|
|
25
|
+
verdict = _check_response(r) if r is not None else None
|
|
26
|
+
if verdict is None:
|
|
27
|
+
return []
|
|
28
|
+
if verdict == "critical":
|
|
29
|
+
return [
|
|
30
|
+
Finding(
|
|
31
|
+
check="cors",
|
|
32
|
+
severity="critical",
|
|
33
|
+
title="Any website can make authenticated requests as your users",
|
|
34
|
+
meaning="Your server tells browsers that evil-hackable-test.example.com "
|
|
35
|
+
"may read responses AND send the victim's cookies along. A malicious "
|
|
36
|
+
"site can silently act as your logged-in users.",
|
|
37
|
+
fix="Never reflect arbitrary origins with Access-Control-Allow-Credentials. "
|
|
38
|
+
"Use an explicit allowlist of your own domains.",
|
|
39
|
+
evidence="Access-Control-Allow-Origin: %s + Allow-Credentials: true" % EVIL,
|
|
40
|
+
url=base + "/",
|
|
41
|
+
)
|
|
42
|
+
]
|
|
43
|
+
if verdict == "high":
|
|
44
|
+
return [
|
|
45
|
+
Finding(
|
|
46
|
+
check="cors",
|
|
47
|
+
severity="high",
|
|
48
|
+
title="CORS trusts arbitrary websites",
|
|
49
|
+
meaning="Your server accepts cross-origin requests from any site I "
|
|
50
|
+
"named. Depending on your auth model, a malicious page could read "
|
|
51
|
+
"private API responses.",
|
|
52
|
+
fix="Replace the wildcard/reflected origin with an explicit allowlist "
|
|
53
|
+
"of domains you own.",
|
|
54
|
+
url=base + "/",
|
|
55
|
+
)
|
|
56
|
+
]
|
|
57
|
+
return [
|
|
58
|
+
Finding(
|
|
59
|
+
check="cors",
|
|
60
|
+
severity="info",
|
|
61
|
+
title="CORS allows any origin (no credentials)",
|
|
62
|
+
meaning="Any website can read your public API responses. That is fine for "
|
|
63
|
+
"public data, but double-check nothing private is served this way.",
|
|
64
|
+
fix="If all responses are public, no action needed. Otherwise restrict "
|
|
65
|
+
"Access-Control-Allow-Origin to your own domains.",
|
|
66
|
+
url=base + "/",
|
|
67
|
+
)
|
|
68
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Debug mode / stack traces exposed on error pages."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
from ..findings import Finding
|
|
6
|
+
|
|
7
|
+
MARKERS = [
|
|
8
|
+
"traceback (most recent call last)",
|
|
9
|
+
"django.core.exceptions",
|
|
10
|
+
"werkzeug.debug",
|
|
11
|
+
"raise valueerror",
|
|
12
|
+
'at ".rb:',
|
|
13
|
+
".java:",
|
|
14
|
+
"system.nullreferenceexception",
|
|
15
|
+
"stack trace:",
|
|
16
|
+
"#0 ",
|
|
17
|
+
"fatal error:",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(base, http):
|
|
22
|
+
probe = "/hackable-probe-" + secrets.token_hex(4)
|
|
23
|
+
r = http.get(base + probe)
|
|
24
|
+
if r is None:
|
|
25
|
+
return []
|
|
26
|
+
body = r.text.lower()
|
|
27
|
+
hits = [m for m in MARKERS if m in body]
|
|
28
|
+
if not hits:
|
|
29
|
+
return []
|
|
30
|
+
return [
|
|
31
|
+
Finding(
|
|
32
|
+
check="debug",
|
|
33
|
+
severity="high",
|
|
34
|
+
title="Error pages leak stack traces",
|
|
35
|
+
meaning="Your 404 page shows internal error details (matched: %s). Stack "
|
|
36
|
+
"traces reveal file paths, library versions and code structure, which "
|
|
37
|
+
"is exactly what an attacker needs to aim." % ", ".join(hits[:3]),
|
|
38
|
+
fix="Turn off debug mode in production and serve a generic error page "
|
|
39
|
+
"that reveals nothing internal. Log the details server-side instead.",
|
|
40
|
+
evidence=r.text.strip().replace("\n", " ")[:160],
|
|
41
|
+
url=base + probe,
|
|
42
|
+
)
|
|
43
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Software version disclosure in response headers."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..findings import Finding
|
|
6
|
+
|
|
7
|
+
VERSION_RE = re.compile(r"\d+\.\d+")
|
|
8
|
+
|
|
9
|
+
INTERESTING = ("server", "x-powered-by", "x-aspnet-version", "x-generator")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(base, http):
|
|
13
|
+
r = http.get(base + "/")
|
|
14
|
+
if r is None:
|
|
15
|
+
return []
|
|
16
|
+
leaked = []
|
|
17
|
+
for name in INTERESTING:
|
|
18
|
+
value = r.headers.get(name)
|
|
19
|
+
if value and VERSION_RE.search(value):
|
|
20
|
+
leaked.append("%s: %s" % (name, value.strip()))
|
|
21
|
+
if not leaked:
|
|
22
|
+
return []
|
|
23
|
+
return [
|
|
24
|
+
Finding(
|
|
25
|
+
check="disclosure",
|
|
26
|
+
severity="low",
|
|
27
|
+
title="Server software versions are public",
|
|
28
|
+
meaning="Your server announces exactly what it runs (%s). Attackers search "
|
|
29
|
+
"for old versions with known holes, so this tells them where to aim."
|
|
30
|
+
% "; ".join(leaked),
|
|
31
|
+
fix="Hide or genericize the Server and X-Powered-By headers in your web "
|
|
32
|
+
"server config. More importantly, keep the software itself updated.",
|
|
33
|
+
evidence="; ".join(leaked),
|
|
34
|
+
url=base + "/",
|
|
35
|
+
)
|
|
36
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Sensitive files accidentally left public."""
|
|
2
|
+
|
|
3
|
+
from ..findings import Finding
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _looks_like_env(text):
|
|
7
|
+
t = text.strip()
|
|
8
|
+
if "<html" in t.lower()[:500]:
|
|
9
|
+
return False
|
|
10
|
+
return "=" in t and any(
|
|
11
|
+
k in t for k in ("KEY", "SECRET", "PASSWORD", "TOKEN", "DATABASE_URL")
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _looks_like_git_head(text):
|
|
16
|
+
return text.strip().startswith("ref:")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _looks_like_git_config(text):
|
|
20
|
+
return "[core]" in text and "repositoryformatversion" in text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _looks_like_apache_status(text):
|
|
24
|
+
return "Apache Status" in text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _looks_like_wp_config(text):
|
|
28
|
+
return "DB_PASSWORD" in text
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _looks_like_ds_store(raw):
|
|
32
|
+
return raw[:8].startswith(b"\x00\x01Bud") or raw[:4] == b"Bud1"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
TARGETS = [
|
|
36
|
+
(
|
|
37
|
+
"/.env",
|
|
38
|
+
"critical",
|
|
39
|
+
lambda text, raw: _looks_like_env(text),
|
|
40
|
+
"Your /.env file is public",
|
|
41
|
+
"This file usually holds database passwords, API keys and app secrets. "
|
|
42
|
+
"Anyone on the internet can now read them and log in as you.",
|
|
43
|
+
"Block /.env (and /.env.*) in your web server config immediately, then "
|
|
44
|
+
"rotate every secret that was inside it. They are all compromised.",
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"/.env.bak",
|
|
48
|
+
"critical",
|
|
49
|
+
lambda text, raw: _looks_like_env(text),
|
|
50
|
+
"A backup of your .env file is public",
|
|
51
|
+
"Same as a public /.env: every secret in it is now in attacker hands.",
|
|
52
|
+
"Delete the backup file from the server and rotate every secret in it.",
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"/.git/HEAD",
|
|
56
|
+
"critical",
|
|
57
|
+
lambda text, raw: _looks_like_git_head(text),
|
|
58
|
+
"Your .git directory is public",
|
|
59
|
+
"Attackers can download your entire source code, including old commits "
|
|
60
|
+
"that may contain secrets you later deleted.",
|
|
61
|
+
"Block /.git/ in your web server config or stop deploying the .git "
|
|
62
|
+
"directory to production.",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"/.git/config",
|
|
66
|
+
"high",
|
|
67
|
+
lambda text, raw: _looks_like_git_config(text),
|
|
68
|
+
"Your git config is public",
|
|
69
|
+
"This confirms your repository internals are exposed and often leads to "
|
|
70
|
+
"full source disclosure.",
|
|
71
|
+
"Block /.git/ in your web server config.",
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"/server-status",
|
|
75
|
+
"high",
|
|
76
|
+
lambda text, raw: _looks_like_apache_status(text),
|
|
77
|
+
"Apache server-status page is public",
|
|
78
|
+
"It shows live requests, client IPs and server internals. Useful for you, "
|
|
79
|
+
"more useful for an attacker mapping your app.",
|
|
80
|
+
"Restrict /server-status to localhost in your Apache config.",
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
"/wp-config.php.bak",
|
|
84
|
+
"critical",
|
|
85
|
+
lambda text, raw: _looks_like_wp_config(text),
|
|
86
|
+
"A WordPress config backup is public",
|
|
87
|
+
"It contains your database username and password in plain text.",
|
|
88
|
+
"Delete the backup file and change the database password.",
|
|
89
|
+
),
|
|
90
|
+
(
|
|
91
|
+
"/.DS_Store",
|
|
92
|
+
"low",
|
|
93
|
+
lambda text, raw: _looks_like_ds_store(raw),
|
|
94
|
+
".DS_Store file is public",
|
|
95
|
+
"A macOS metadata file. It can reveal file and folder names on your server.",
|
|
96
|
+
"Stop deploying .DS_Store files; add them to .gitignore.",
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run(base, http):
|
|
102
|
+
findings = []
|
|
103
|
+
for path, severity, signature, title, meaning, fix in TARGETS:
|
|
104
|
+
r = http.get(base + path)
|
|
105
|
+
if r is None or r.status_code != 200:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
if signature(r.text, r.content):
|
|
109
|
+
findings.append(
|
|
110
|
+
Finding(
|
|
111
|
+
check="exposed_files",
|
|
112
|
+
severity=severity,
|
|
113
|
+
title=title,
|
|
114
|
+
meaning=meaning,
|
|
115
|
+
fix=fix,
|
|
116
|
+
evidence=r.text.strip().replace("\n", " ")[:120],
|
|
117
|
+
url=base + path,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
except Exception:
|
|
121
|
+
continue
|
|
122
|
+
return findings
|