thuban 0.4.0 → 0.4.1
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.
- package/dist/packages/scanner/executive-report.js +1 -1
- package/dist/packages/scanner/hallucination-detector.js +1 -1
- package/dist/packages/scanner/secret-scanner.js +1 -1
- package/package.json +4 -3
- package/dist/packages/crucible/seeds/python/seed-021 3.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-022 3.py +0 -34
- package/dist/packages/crucible/seeds/python/seed-023 3.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-024 3.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-025 3.py +0 -40
- package/dist/packages/crucible/seeds/python/seed-026 3.py +0 -35
- package/dist/packages/crucible/seeds/python/seed-027 3.py +0 -35
- package/dist/packages/crucible/seeds/python/seed-028 3.py +0 -42
- package/dist/packages/crucible/seeds/python/seed-030 3.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-031 3.py +0 -34
- package/dist/packages/crucible/seeds/python/seed-036 3.py +0 -33
- package/dist/packages/crucible/seeds/python/seed-037 3.py +0 -41
- package/dist/packages/crucible/seeds/python/seed-038 3.py +0 -33
- package/dist/packages/crucible/seeds/python/seed-039 3.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-040 3.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-041 3.py +0 -37
- package/dist/packages/crucible/seeds/python/seed-042 3.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-043 3.py +0 -32
- package/dist/packages/crucible/seeds/python/seed-044 3.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-045 3.py +0 -36
- package/dist/packages/crucible/seeds/python/seed-046 3.py +0 -33
- package/dist/packages/crucible/seeds/python/seed-047 3.py +0 -44
- package/dist/packages/crucible/seeds/python/seed-048 3.py +0 -35
- package/dist/packages/crucible/seeds/python/seed-049 3.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-050 3.py +0 -39
- package/dist/packages/crucible/seeds/python/seed-051 3.py +0 -38
- package/dist/packages/crucible/seeds/python/seed-052 3.py +0 -41
- package/dist/packages/crucible/seeds/ruby/seed-001 3.rb +0 -15
- package/dist/packages/crucible/seeds/ruby/seed-002 3.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-003 3.rb +0 -25
- package/dist/packages/crucible/seeds/ruby/seed-004 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-005 3.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-006 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-007 3.rb +0 -16
- package/dist/packages/crucible/seeds/ruby/seed-008 3.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-009 3.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-010 3.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-011 3.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-012 3.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-013 3.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-014 3.rb +0 -16
- package/dist/packages/crucible/seeds/ruby/seed-015 3.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-016 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-017 3.rb +0 -25
- package/dist/packages/crucible/seeds/ruby/seed-018 3.rb +0 -23
- package/dist/packages/crucible/seeds/ruby/seed-019 3.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-020 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-021 3.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-022 3.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-023 3.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-024 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-025 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-026 3.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-027 3.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-028 3.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-029 3.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-030 3.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-031 3.rb +0 -17
- package/dist/packages/crucible/seeds/ruby/seed-032 3.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-033 3.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-034 3.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-035 3.rb +0 -19
- package/dist/packages/crucible/seeds/ruby/seed-036 3.rb +0 -18
- package/dist/packages/crucible/seeds/ruby/seed-037 3.rb +0 -21
- package/dist/packages/crucible/seeds/ruby/seed-038 3.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-039 3.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-040 3.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-041 3.rb +0 -23
- package/dist/packages/crucible/seeds/ruby/seed-042 3.rb +0 -25
- package/dist/packages/crucible/seeds/ruby/seed-043 3.rb +0 -23
- package/dist/packages/crucible/seeds/ruby/seed-044 3.rb +0 -16
- package/dist/packages/crucible/seeds/ruby/seed-045 3.rb +0 -22
- package/dist/packages/crucible/seeds/ruby/seed-046 3.rb +0 -27
- package/dist/packages/crucible/seeds/ruby/seed-047 3.rb +0 -26
- package/dist/packages/crucible/seeds/ruby/seed-048 3.rb +0 -24
- package/dist/packages/crucible/seeds/ruby/seed-049 3.rb +0 -20
- package/dist/packages/crucible/seeds/ruby/seed-050 3.rb +0 -27
- package/dist/packages/crucible/seeds/rust/seed-001 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-002 3.rs +0 -20
- package/dist/packages/crucible/seeds/rust/seed-003 3.rs +0 -23
- package/dist/packages/crucible/seeds/rust/seed-004 3.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-005 3.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-006 3.rs +0 -24
- package/dist/packages/crucible/seeds/rust/seed-007 3.rs +0 -20
- package/dist/packages/crucible/seeds/rust/seed-008 3.rs +0 -19
- package/dist/packages/crucible/seeds/rust/seed-009 3.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-010 3.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-011 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-012 3.rs +0 -31
- package/dist/packages/crucible/seeds/rust/seed-013 3.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-014 3.rs +0 -30
- package/dist/packages/crucible/seeds/rust/seed-015 3.rs +0 -33
- package/dist/packages/crucible/seeds/rust/seed-016 3.rs +0 -22
- package/dist/packages/crucible/seeds/rust/seed-017 3.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-018 3.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-019 3.rs +0 -36
- package/dist/packages/crucible/seeds/rust/seed-020 3.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-021 3.rs +0 -26
- package/dist/packages/crucible/seeds/rust/seed-022 3.rs +0 -23
- package/dist/packages/crucible/seeds/rust/seed-023 3.rs +0 -22
- package/dist/packages/crucible/seeds/rust/seed-024 3.rs +0 -24
- package/dist/packages/crucible/seeds/rust/seed-025 3.rs +0 -29
- package/dist/packages/crucible/seeds/rust/seed-026 3.rs +0 -23
- package/dist/packages/crucible/seeds/rust/seed-027 3.rs +0 -24
- package/dist/packages/crucible/seeds/rust/seed-028 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-029 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-030 3.rs +0 -30
- package/dist/packages/crucible/seeds/rust/seed-031 3.rs +0 -22
- package/dist/packages/crucible/seeds/rust/seed-032 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-033 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-034 3.rs +0 -20
- package/dist/packages/crucible/seeds/rust/seed-035 3.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-036 3.rs +0 -26
- package/dist/packages/crucible/seeds/rust/seed-037 3.rs +0 -31
- package/dist/packages/crucible/seeds/rust/seed-038 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-039 3.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-040 3.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-041 3.rs +0 -32
- package/dist/packages/crucible/seeds/rust/seed-042 3.rs +0 -27
- package/dist/packages/crucible/seeds/rust/seed-043 3.rs +0 -29
- package/dist/packages/crucible/seeds/rust/seed-044 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-045 3.rs +0 -28
- package/dist/packages/crucible/seeds/rust/seed-046 3.rs +0 -25
- package/dist/packages/crucible/seeds/rust/seed-047 3.rs +0 -34
- package/dist/packages/crucible/seeds/rust/seed-048 3.rs +0 -21
- package/dist/packages/crucible/seeds/rust/seed-049 3.rs +0 -26
- package/dist/packages/crucible/seeds/rust/seed-050 3.rs +0 -23
- package/dist/packages/crucible/seeds/ts/seed-001 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-002 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-003 3.ts +0 -28
- package/dist/packages/crucible/seeds/ts/seed-004 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-005 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-006 3.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-007 3.ts +0 -28
- package/dist/packages/crucible/seeds/ts/seed-008 3.ts +0 -40
- package/dist/packages/crucible/seeds/ts/seed-009 3.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-010 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-011 3.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-012 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-013 3.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-014 3.ts +0 -36
- package/dist/packages/crucible/seeds/ts/seed-015 3.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-016 3.ts +0 -37
- package/dist/packages/crucible/seeds/ts/seed-017 3.ts +0 -44
- package/dist/packages/crucible/seeds/ts/seed-018 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-019 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-020 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-021 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-022 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-023 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-024 3.ts +0 -35
- package/dist/packages/crucible/seeds/ts/seed-025 3.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-026 3.ts +0 -36
- package/dist/packages/crucible/seeds/ts/seed-027 3.ts +0 -30
- package/dist/packages/crucible/seeds/ts/seed-028 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-029 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-030 3.ts +0 -37
- package/dist/packages/crucible/seeds/ts/seed-031 3.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-032 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-033 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-034 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-035 3.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-036 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-037 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-038 3.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-039 3.ts +0 -35
- package/dist/packages/crucible/seeds/ts/seed-040 3.ts +0 -31
- package/dist/packages/crucible/seeds/ts/seed-041 3.ts +0 -29
- package/dist/packages/crucible/seeds/ts/seed-042 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-043 3.ts +0 -32
- package/dist/packages/crucible/seeds/ts/seed-044 3.ts +0 -36
- package/dist/packages/crucible/seeds/ts/seed-045 3.ts +0 -39
- package/dist/packages/crucible/seeds/ts/seed-046 3.ts +0 -41
- package/dist/packages/crucible/seeds/ts/seed-047 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-048 3.ts +0 -33
- package/dist/packages/crucible/seeds/ts/seed-049 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-050 3.ts +0 -41
- package/dist/packages/crucible/seeds/ts/seed-051 3.ts +0 -34
- package/dist/packages/crucible/seeds/ts/seed-052 3.ts +0 -36
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: command_injection
|
|
3
|
-
# severity: critical
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: command injection via subprocess.Popen with shell=True and f-string
|
|
6
|
-
|
|
7
|
-
import subprocess
|
|
8
|
-
from fastapi import FastAPI
|
|
9
|
-
from pydantic import BaseModel
|
|
10
|
-
|
|
11
|
-
app = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ImageRequest(BaseModel):
|
|
15
|
-
image_path: str # user-controlled
|
|
16
|
-
width: int
|
|
17
|
-
height: int
|
|
18
|
-
quality: int = 85
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ReportRequest(BaseModel):
|
|
22
|
-
format: str # user-controlled
|
|
23
|
-
date_range: str # user-controlled
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@app.post("/images/thumbnail")
|
|
27
|
-
def generate_thumbnail(req: ImageRequest):
|
|
28
|
-
# image_path is user-controlled — injection via shell metacharacters
|
|
29
|
-
cmd = f"ffmpeg -i {req.image_path} -vf scale={req.width}:{req.height} -q:v {req.quality} out.jpg"
|
|
30
|
-
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
31
|
-
out, err = proc.communicate(timeout=30)
|
|
32
|
-
return {"output": out.decode(), "errors": err.decode()}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@app.post("/reports/generate")
|
|
36
|
-
def generate_report(req: ReportRequest):
|
|
37
|
-
cmd = f"python3 scripts/report.py --format {req.format} --range {req.date_range}"
|
|
38
|
-
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
|
|
39
|
-
out, _ = proc.communicate()
|
|
40
|
-
return {"path": out.decode().strip()}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: command_injection
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: command injection via os.popen with user-controlled filename
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
from flask import Flask, request, send_file
|
|
9
|
-
import io
|
|
10
|
-
|
|
11
|
-
app = Flask(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.route("/export/csv")
|
|
15
|
-
def export_csv():
|
|
16
|
-
table = request.args.get("table", "users")
|
|
17
|
-
date = request.args.get("date", "2024-01-01")
|
|
18
|
-
# table and date injected via shell — attacker: table = "users; id > /tmp/pwned"
|
|
19
|
-
output = os.popen(f"psql -c 'SELECT * FROM {table} WHERE date > \"{date}\"' -A -F, mydb").read()
|
|
20
|
-
return output, 200, {"Content-Type": "text/csv"}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@app.route("/preview/pdf")
|
|
24
|
-
def preview_pdf():
|
|
25
|
-
filename = request.args.get("file", "")
|
|
26
|
-
# filename from user — injection via backticks or semicolons
|
|
27
|
-
output = os.popen(f"pdftotext uploads/{filename} -").read()
|
|
28
|
-
return {"text": output}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@app.route("/image/metadata")
|
|
32
|
-
def image_metadata():
|
|
33
|
-
path = request.args.get("path", "")
|
|
34
|
-
result = os.popen(f"exiftool {path}").read()
|
|
35
|
-
return {"metadata": result}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: command_injection
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: command injection via paramiko exec_command with user-controlled command string
|
|
6
|
-
|
|
7
|
-
import paramiko
|
|
8
|
-
from flask import Flask, request, jsonify
|
|
9
|
-
|
|
10
|
-
app = Flask(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_ssh_client(host: str) -> paramiko.SSHClient:
|
|
14
|
-
client = paramiko.SSHClient()
|
|
15
|
-
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
16
|
-
client.connect(host, username="deploy", key_filename="/home/deploy/.ssh/id_rsa")
|
|
17
|
-
return client
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@app.route("/ssh/run", methods=["POST"])
|
|
21
|
-
def run_remote_command():
|
|
22
|
-
data = request.get_json()
|
|
23
|
-
host = data.get("host", "")
|
|
24
|
-
# command from user — attacker appends malicious commands
|
|
25
|
-
command = data.get("command", "")
|
|
26
|
-
allowed_hosts = ["web-01.internal", "web-02.internal"]
|
|
27
|
-
if host not in allowed_hosts:
|
|
28
|
-
return jsonify({"error": "Host not allowed"}), 403
|
|
29
|
-
|
|
30
|
-
client = get_ssh_client(host)
|
|
31
|
-
# command not validated — attacker sends "ls; cat /etc/shadow"
|
|
32
|
-
stdin, stdout, stderr = client.exec_command(command)
|
|
33
|
-
output = stdout.read().decode()
|
|
34
|
-
client.close()
|
|
35
|
-
return jsonify({"output": output})
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: command_injection
|
|
3
|
-
# severity: critical
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: command injection via subprocess.run with shell=True and user-controlled git operation
|
|
6
|
-
|
|
7
|
-
import subprocess
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class GitConfig:
|
|
13
|
-
repo_path: str
|
|
14
|
-
branch: str # user-controlled
|
|
15
|
-
remote: str = "origin"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class GitService:
|
|
19
|
-
def clone(self, repo_url: str, target: str) -> str:
|
|
20
|
-
# repo_url from user — injection possible
|
|
21
|
-
result = subprocess.run(
|
|
22
|
-
f"git clone {repo_url} {target}", shell=True, capture_output=True, text=True
|
|
23
|
-
)
|
|
24
|
-
return result.stdout
|
|
25
|
-
|
|
26
|
-
def checkout(self, cfg: GitConfig) -> str:
|
|
27
|
-
# cfg.branch from user — "main; cat /etc/passwd > /tmp/leak"
|
|
28
|
-
result = subprocess.run(
|
|
29
|
-
f"git -C {cfg.repo_path} checkout {cfg.branch}",
|
|
30
|
-
shell=True, capture_output=True, text=True
|
|
31
|
-
)
|
|
32
|
-
return result.stdout
|
|
33
|
-
|
|
34
|
-
def tag(self, repo: str, tag_name: str, message: str) -> str:
|
|
35
|
-
result = subprocess.run(
|
|
36
|
-
f'git -C {repo} tag -a {tag_name} -m "{message}"',
|
|
37
|
-
shell=True, capture_output=True, text=True
|
|
38
|
-
)
|
|
39
|
-
return result.stdout
|
|
40
|
-
|
|
41
|
-
def archive(self, repo: str, branch: str, output_file: str) -> None:
|
|
42
|
-
subprocess.run(f"git -C {repo} archive {branch} > {output_file}", shell=True)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: path_traversal
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: path traversal in Django view via user-controlled filename parameter
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
from django.http import FileResponse, Http404, HttpResponse
|
|
9
|
-
from django.conf import settings
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def serve_user_file(request, user_id: str):
|
|
13
|
-
filename = request.GET.get("file", "")
|
|
14
|
-
# No normalization — filename can be "../../../etc/passwd"
|
|
15
|
-
file_path = os.path.join(settings.MEDIA_ROOT, user_id, filename)
|
|
16
|
-
try:
|
|
17
|
-
return FileResponse(open(file_path, "rb"))
|
|
18
|
-
except FileNotFoundError:
|
|
19
|
-
raise Http404("File not found")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def export_report(request):
|
|
23
|
-
report_name = request.GET.get("report", "monthly")
|
|
24
|
-
locale = request.GET.get("locale", "en")
|
|
25
|
-
# Both report_name and locale inserted into path — traversal possible
|
|
26
|
-
path = os.path.join(settings.REPORTS_DIR, locale, f"{report_name}.pdf")
|
|
27
|
-
if not os.path.exists(path):
|
|
28
|
-
raise Http404
|
|
29
|
-
with open(path, "rb") as f:
|
|
30
|
-
return HttpResponse(f.read(), content_type="application/pdf")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def read_template(request):
|
|
34
|
-
template = request.GET.get("template", "welcome")
|
|
35
|
-
path = f"{settings.TEMPLATES_DIR}/{template}.html"
|
|
36
|
-
with open(path) as f:
|
|
37
|
-
return HttpResponse(f.read())
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: path_traversal
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: path traversal via zipfile extraction without entry path validation (zip-slip)
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
import zipfile
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
EXTRACT_BASE = "/var/app/uploads/extracted"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def extract_archive(zip_bytes: bytes, user_id: str) -> list:
|
|
15
|
-
import io
|
|
16
|
-
extracted = []
|
|
17
|
-
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
18
|
-
for entry in zf.infolist():
|
|
19
|
-
# entry.filename can contain "../" — writes outside EXTRACT_BASE
|
|
20
|
-
output_path = os.path.join(EXTRACT_BASE, user_id, entry.filename)
|
|
21
|
-
if entry.is_dir():
|
|
22
|
-
os.makedirs(output_path, exist_ok=True)
|
|
23
|
-
else:
|
|
24
|
-
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
25
|
-
with zf.open(entry) as src, open(output_path, "wb") as dst:
|
|
26
|
-
dst.write(src.read())
|
|
27
|
-
extracted.append(output_path)
|
|
28
|
-
return extracted
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_extracted_file(user_id: str, relative_path: str) -> bytes:
|
|
32
|
-
full_path = os.path.join(EXTRACT_BASE, user_id, relative_path)
|
|
33
|
-
with open(full_path, "rb") as f:
|
|
34
|
-
return f.read()
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: insecure_crypto
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: DES cipher usage and AES-ECB mode with static key
|
|
6
|
-
|
|
7
|
-
from Crypto.Cipher import DES, AES
|
|
8
|
-
import base64
|
|
9
|
-
|
|
10
|
-
DES_KEY = b"weakkey!" # 8 bytes — DES is broken
|
|
11
|
-
AES_KEY = b"0123456789abcdef" # 16 bytes
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def des_encrypt(data: str) -> str:
|
|
15
|
-
# DES has only 56-bit effective key length — trivially brute-forced
|
|
16
|
-
padded = data + " " * (8 - len(data) % 8)
|
|
17
|
-
cipher = DES.new(DES_KEY, DES.MODE_ECB)
|
|
18
|
-
return base64.b64encode(cipher.encrypt(padded.encode())).decode()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def aes_ecb_encrypt(data: str) -> str:
|
|
22
|
-
# ECB mode: identical plaintext blocks produce identical ciphertext blocks
|
|
23
|
-
pad_len = 16 - len(data) % 16
|
|
24
|
-
padded = (data + chr(pad_len) * pad_len).encode()
|
|
25
|
-
cipher = AES.new(AES_KEY, AES.MODE_ECB)
|
|
26
|
-
return base64.b64encode(cipher.encrypt(padded)).decode()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def aes_ecb_decrypt(ciphertext: str) -> str:
|
|
30
|
-
cipher = AES.new(AES_KEY, AES.MODE_ECB)
|
|
31
|
-
decrypted = cipher.decrypt(base64.b64decode(ciphertext))
|
|
32
|
-
pad_len = decrypted[-1]
|
|
33
|
-
return decrypted[:-pad_len].decode()
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: insecure_crypto
|
|
3
|
-
# severity: medium
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: timing attack via string comparison of HMAC signatures, low-iteration PBKDF2
|
|
6
|
-
|
|
7
|
-
import hmac
|
|
8
|
-
import hashlib
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
WEBHOOK_SECRET = b"static-webhook-secret-2024"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def compute_signature(payload: bytes) -> str:
|
|
15
|
-
return hmac.new(WEBHOOK_SECRET, payload, hashlib.sha256).hexdigest()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def verify_signature(payload: bytes, received_sig: str) -> bool:
|
|
19
|
-
expected = compute_signature(payload)
|
|
20
|
-
# String comparison leaks timing info — use hmac.compare_digest instead
|
|
21
|
-
return expected == received_sig
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def verify_api_key(provided: str, stored: str) -> bool:
|
|
25
|
-
# Direct equality — timing side-channel attack possible
|
|
26
|
-
return provided == stored
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def derive_key(password: str) -> bytes:
|
|
30
|
-
# Only 1000 iterations — far below NIST recommendation of 600,000+
|
|
31
|
-
return hashlib.pbkdf2_hmac("sha1", password.encode(), b"static-salt", 1000)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def check_token(token_a: str, token_b: str) -> bool:
|
|
35
|
-
# Char-by-char comparison leaks prefix matches
|
|
36
|
-
if len(token_a) != len(token_b):
|
|
37
|
-
return False
|
|
38
|
-
for a, b in zip(token_a, token_b):
|
|
39
|
-
if a != b:
|
|
40
|
-
return False
|
|
41
|
-
return True
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: insecure_crypto
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: MD5 used for file integrity and RSA with insufficient key size
|
|
6
|
-
|
|
7
|
-
import hashlib
|
|
8
|
-
from Crypto.PublicKey import RSA
|
|
9
|
-
from Crypto.Signature import pkcs1_15
|
|
10
|
-
from Crypto.Hash import MD5
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def checksum_file(filepath: str) -> str:
|
|
14
|
-
# MD5 is collision-prone — not suitable for integrity verification
|
|
15
|
-
with open(filepath, "rb") as f:
|
|
16
|
-
return hashlib.md5(f.read()).hexdigest()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def verify_file_integrity(filepath: str, expected_md5: str) -> bool:
|
|
20
|
-
return checksum_file(filepath) == expected_md5
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def generate_rsa_keypair():
|
|
24
|
-
# 512-bit RSA key — trivially factorable in seconds
|
|
25
|
-
key = RSA.generate(512)
|
|
26
|
-
return key.export_key().decode(), key.publickey().export_key().decode()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def sign_with_md5(private_key_pem: str, message: bytes) -> bytes:
|
|
30
|
-
key = RSA.import_key(private_key_pem)
|
|
31
|
-
# MD5 with RSA — known-weak combination
|
|
32
|
-
h = MD5.new(message)
|
|
33
|
-
return pkcs1_15.new(key).sign(h)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: eval_abuse
|
|
3
|
-
# severity: critical
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: eval() called with user-controlled expression string
|
|
6
|
-
|
|
7
|
-
from flask import Flask, request, jsonify
|
|
8
|
-
|
|
9
|
-
app = Flask(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@app.route("/calculate", methods=["POST"])
|
|
13
|
-
def calculate():
|
|
14
|
-
data = request.get_json()
|
|
15
|
-
expression = data.get("expression", "")
|
|
16
|
-
try:
|
|
17
|
-
# Attacker sends: "__import__('os').system('id')"
|
|
18
|
-
result = eval(expression)
|
|
19
|
-
return jsonify({"result": result})
|
|
20
|
-
except Exception as e:
|
|
21
|
-
return jsonify({"error": str(e)}), 400
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@app.route("/filter", methods=["POST"])
|
|
25
|
-
def filter_data():
|
|
26
|
-
data = request.get_json()
|
|
27
|
-
records = data.get("records", [])
|
|
28
|
-
condition = data.get("condition", "True")
|
|
29
|
-
# Attacker sends: "True or __import__('subprocess').check_output(['id'])"
|
|
30
|
-
filtered = [r for r in records if eval(condition, {"r": r})]
|
|
31
|
-
return jsonify({"results": filtered})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@app.route("/transform", methods=["POST"])
|
|
35
|
-
def transform():
|
|
36
|
-
expr = request.get_json().get("expr", "x")
|
|
37
|
-
items = request.get_json().get("items", [1, 2, 3])
|
|
38
|
-
result = list(map(lambda x: eval(expr), items))
|
|
39
|
-
return jsonify({"result": result})
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: eval_abuse
|
|
3
|
-
# severity: critical
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: exec() called with user-controlled code string
|
|
6
|
-
|
|
7
|
-
from flask import Flask, request, jsonify
|
|
8
|
-
|
|
9
|
-
app = Flask(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@app.route("/scripting/run", methods=["POST"])
|
|
13
|
-
def run_script():
|
|
14
|
-
data = request.get_json()
|
|
15
|
-
code = data.get("code", "")
|
|
16
|
-
context = data.get("context", {})
|
|
17
|
-
local_vars = {"result": None, **context}
|
|
18
|
-
try:
|
|
19
|
-
# exec() executes arbitrary Python — no sandboxing
|
|
20
|
-
exec(code, {"__builtins__": __builtins__}, local_vars)
|
|
21
|
-
return jsonify({"result": local_vars.get("result")})
|
|
22
|
-
except Exception as e:
|
|
23
|
-
return jsonify({"error": str(e)}), 400
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@app.route("/hooks/execute", methods=["POST"])
|
|
27
|
-
def execute_hook():
|
|
28
|
-
hook_code = request.get_json().get("hook", "")
|
|
29
|
-
event = request.get_json().get("event", {})
|
|
30
|
-
# User-controlled hook code executed server-side
|
|
31
|
-
exec(hook_code, {"event": event, "print": print})
|
|
32
|
-
return jsonify({"executed": True})
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@app.route("/migration/run", methods=["POST"])
|
|
36
|
-
def run_migration():
|
|
37
|
-
migration_script = request.get_json().get("script", "")
|
|
38
|
-
exec(compile(migration_script, "<migration>", "exec"))
|
|
39
|
-
return jsonify({"status": "done"})
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: eval_abuse
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: eval() used in Jinja2-like custom template rendering with user template string
|
|
6
|
-
|
|
7
|
-
import re
|
|
8
|
-
from flask import Flask, request
|
|
9
|
-
|
|
10
|
-
app = Flask(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def render_template(template: str, context: dict) -> str:
|
|
14
|
-
"""Simple template renderer that evaluates {{ expr }} blocks."""
|
|
15
|
-
def evaluate(match):
|
|
16
|
-
expr = match.group(1).strip()
|
|
17
|
-
# eval with user-controlled expression — attacker injects arbitrary Python
|
|
18
|
-
return str(eval(expr, {"__builtins__": {}}, context))
|
|
19
|
-
|
|
20
|
-
return re.sub(r"\{\{(.*?)\}\}", evaluate, template)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@app.route("/preview", methods=["POST"])
|
|
24
|
-
def preview():
|
|
25
|
-
data = request.get_json()
|
|
26
|
-
template = data.get("template", "Hello {{ name }}!")
|
|
27
|
-
ctx = data.get("context", {"name": "World"})
|
|
28
|
-
# Attacker template: "{{ __import__('os').system('id') }}"
|
|
29
|
-
output = render_template(template, ctx)
|
|
30
|
-
return output
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@app.route("/report", methods=["POST"])
|
|
34
|
-
def report():
|
|
35
|
-
tpl = request.form.get("template", "")
|
|
36
|
-
rendered = render_template(tpl, {"user": request.form.get("user", "guest")})
|
|
37
|
-
return rendered
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: eval_abuse
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: eval() in pandas query/eval with user-controlled expression
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from flask import Flask, request, jsonify
|
|
9
|
-
|
|
10
|
-
app = Flask(__name__)
|
|
11
|
-
|
|
12
|
-
# Simulated data store
|
|
13
|
-
DATA = pd.DataFrame({
|
|
14
|
-
"id": range(1, 101),
|
|
15
|
-
"amount": [i * 10.5 for i in range(1, 101)],
|
|
16
|
-
"category": ["A" if i % 2 == 0 else "B" for i in range(1, 101)],
|
|
17
|
-
"active": [True] * 100,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@app.route("/data/query", methods=["POST"])
|
|
22
|
-
def query_data():
|
|
23
|
-
body = request.get_json()
|
|
24
|
-
filter_expr = body.get("filter", "active == True")
|
|
25
|
-
# pd.DataFrame.query() can execute arbitrary Python when filter_expr is user-controlled
|
|
26
|
-
# Attacker: "@__import__('os').system('id')"
|
|
27
|
-
try:
|
|
28
|
-
result = DATA.query(filter_expr)
|
|
29
|
-
return jsonify({"count": len(result), "data": result.to_dict("records")})
|
|
30
|
-
except Exception as e:
|
|
31
|
-
return jsonify({"error": str(e)}), 400
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@app.route("/data/eval", methods=["POST"])
|
|
35
|
-
def eval_expression():
|
|
36
|
-
expr = request.get_json().get("expr", "amount * 2")
|
|
37
|
-
result = DATA.eval(expr)
|
|
38
|
-
return jsonify({"result": result.tolist()})
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: eval_abuse
|
|
3
|
-
# severity: critical
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: eval()/compile() used to execute user-uploaded Python module code
|
|
6
|
-
|
|
7
|
-
import ast
|
|
8
|
-
from flask import Flask, request, jsonify
|
|
9
|
-
|
|
10
|
-
app = Flask(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def safe_eval(code: str, context: dict) -> object:
|
|
14
|
-
"""Attempts to use AST to 'safely' evaluate — but compile+exec bypass this."""
|
|
15
|
-
try:
|
|
16
|
-
tree = ast.parse(code, mode="eval")
|
|
17
|
-
# compile() + eval() of user AST still allows dangerous constructs
|
|
18
|
-
compiled = compile(tree, "<user-input>", "eval")
|
|
19
|
-
return eval(compiled, {"__builtins__": __builtins__}, context)
|
|
20
|
-
except SyntaxError:
|
|
21
|
-
# Falls back to exec for multi-line code
|
|
22
|
-
exec(compile(code, "<user-input>", "exec"), context)
|
|
23
|
-
return context.get("result")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@app.route("/plugin/evaluate", methods=["POST"])
|
|
27
|
-
def evaluate_plugin():
|
|
28
|
-
payload = request.get_json()
|
|
29
|
-
code = payload.get("code", "")
|
|
30
|
-
params = payload.get("params", {})
|
|
31
|
-
result = safe_eval(code, params)
|
|
32
|
-
return jsonify({"output": result})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: ssrf
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: SSRF via requests.get with unvalidated user-supplied URL
|
|
6
|
-
|
|
7
|
-
import requests
|
|
8
|
-
from flask import Flask, request, jsonify
|
|
9
|
-
|
|
10
|
-
app = Flask(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@app.route("/proxy")
|
|
14
|
-
def proxy():
|
|
15
|
-
target_url = request.args.get("url", "")
|
|
16
|
-
# No validation — attacker requests: http://169.254.169.254/latest/meta-data/
|
|
17
|
-
response = requests.get(target_url, timeout=10)
|
|
18
|
-
return jsonify({
|
|
19
|
-
"status": response.status_code,
|
|
20
|
-
"content": response.text[:2000],
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@app.route("/webhook/test", methods=["POST"])
|
|
25
|
-
def test_webhook():
|
|
26
|
-
data = request.get_json()
|
|
27
|
-
webhook_url = data.get("url", "")
|
|
28
|
-
payload = data.get("payload", {})
|
|
29
|
-
# webhook_url from user — attacker targets internal services
|
|
30
|
-
resp = requests.post(webhook_url, json=payload, timeout=8)
|
|
31
|
-
return jsonify({"status": resp.status_code, "response": resp.text[:500]})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@app.route("/avatar/fetch")
|
|
35
|
-
def fetch_avatar():
|
|
36
|
-
avatar_url = request.args.get("url", "")
|
|
37
|
-
resp = requests.get(avatar_url, timeout=5, allow_redirects=True)
|
|
38
|
-
return resp.content, 200, {"Content-Type": resp.headers.get("content-type", "image/png")}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: ssrf
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: SSRF via httpx.get with user-controlled URL and redirect following
|
|
6
|
-
|
|
7
|
-
import httpx
|
|
8
|
-
from fastapi import FastAPI, Query
|
|
9
|
-
from fastapi.responses import JSONResponse
|
|
10
|
-
|
|
11
|
-
app = FastAPI()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.get("/import/data")
|
|
15
|
-
async def import_data(source: str = Query(...)):
|
|
16
|
-
# source from user — can be internal URL or cloud metadata endpoint
|
|
17
|
-
async with httpx.AsyncClient(follow_redirects=True, timeout=10.0) as client:
|
|
18
|
-
resp = await client.get(source)
|
|
19
|
-
return JSONResponse({"status": resp.status_code, "preview": resp.text[:500]})
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@app.post("/oembed")
|
|
23
|
-
async def fetch_oembed(url: str = Query(...), provider: str = Query(...)):
|
|
24
|
-
# provider is an oembed endpoint from user — SSRF to internal services
|
|
25
|
-
oembed_url = f"{provider}?url={url}&format=json"
|
|
26
|
-
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
27
|
-
resp = await client.get(oembed_url)
|
|
28
|
-
return resp.json()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@app.get("/link-preview")
|
|
32
|
-
async def link_preview(target: str = Query(...)):
|
|
33
|
-
async with httpx.AsyncClient(timeout=5.0, follow_redirects=True) as client:
|
|
34
|
-
resp = await client.get(target)
|
|
35
|
-
title = resp.text.split("<title>")[1].split("</title>")[0] if "<title>" in resp.text else ""
|
|
36
|
-
return {"title": title, "url": target}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# [CRUCIBLE-SEED]
|
|
2
|
-
# category: ssrf
|
|
3
|
-
# severity: high
|
|
4
|
-
# language: python
|
|
5
|
-
# expected: SSRF via urllib.request.urlopen with unvalidated user URL
|
|
6
|
-
|
|
7
|
-
import urllib.request
|
|
8
|
-
import urllib.parse
|
|
9
|
-
from django.http import JsonResponse
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def fetch_remote_resource(request):
|
|
13
|
-
"""Fetch an external resource on behalf of the client."""
|
|
14
|
-
url = request.GET.get("url", "")
|
|
15
|
-
|
|
16
|
-
# No validation — internal URLs, file://, gopher:// etc. all accepted
|
|
17
|
-
try:
|
|
18
|
-
with urllib.request.urlopen(url, timeout=10) as response:
|
|
19
|
-
content = response.read(4096).decode("utf-8", errors="replace")
|
|
20
|
-
content_type = response.headers.get("Content-Type", "")
|
|
21
|
-
return JsonResponse({"content": content, "content_type": content_type})
|
|
22
|
-
except Exception as e:
|
|
23
|
-
return JsonResponse({"error": str(e)}, status=400)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def check_url_status(request):
|
|
27
|
-
target = request.POST.get("target", "")
|
|
28
|
-
try:
|
|
29
|
-
req = urllib.request.Request(target, headers={"User-Agent": "StatusChecker/1.0"})
|
|
30
|
-
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
31
|
-
return JsonResponse({"status": resp.status, "url": target})
|
|
32
|
-
except urllib.error.HTTPError as e:
|
|
33
|
-
return JsonResponse({"status": e.code})
|