hardened CSRF, SSRF, FTS5
- CSRF: Generate random token at startup, include as hidden field in all 11 POST forms, validate at top of POST dispatch (returns 403) - SSRF: Block private/internal IP ranges (127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7) by resolving hostname before fetch. Remove verify=False from requests.get(). - DELETE: Change /delete/<id> from GET (instant delete) to GET (confirmation page) + POST (actual delete) to prevent accidental deletion from prefetchers/crawlers. - FTS5: Wrap search input in double quotes to neutralize FTS5 operators (AND, OR, NOT, *, column:). Add try/except fallback.
This commit is contained in:
parent
104bb7ba2d
commit
0981c2e0a9
2 changed files with 106 additions and 20 deletions
35
db.py
35
db.py
|
|
@ -1,3 +1,5 @@
|
|||
import socket
|
||||
import ipaddress
|
||||
import sqlite3
|
||||
import requests
|
||||
from urllib.parse import urlparse, urljoin, parse_qs, urlencode, urlunparse
|
||||
|
|
@ -5,6 +7,36 @@ from bs4 import BeautifulSoup
|
|||
|
||||
DATABASE = "index.db"
|
||||
|
||||
BLOCKED_NETWORKS = [
|
||||
ipaddress.ip_network("127.0.0.0/8"),
|
||||
ipaddress.ip_network("10.0.0.0/8"),
|
||||
ipaddress.ip_network("172.16.0.0/12"),
|
||||
ipaddress.ip_network("192.168.0.0/16"),
|
||||
ipaddress.ip_network("169.254.0.0/16"),
|
||||
ipaddress.ip_network("0.0.0.0/8"),
|
||||
ipaddress.ip_network("::1/128"),
|
||||
ipaddress.ip_network("fc00::/7"),
|
||||
ipaddress.ip_network("fe80::/10"),
|
||||
]
|
||||
|
||||
|
||||
def _validate_url_target(url):
|
||||
"""Resolve hostname and block private/internal IPs to prevent SSRF."""
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||
if not hostname:
|
||||
raise ValueError(f"No hostname in URL: {url}")
|
||||
try:
|
||||
addrs = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP)
|
||||
except socket.gaierror:
|
||||
raise ValueError(f"Cannot resolve hostname: {hostname}")
|
||||
for family, type_, proto, canonname, sockaddr in addrs:
|
||||
ip = ipaddress.ip_address(sockaddr[0])
|
||||
for network in BLOCKED_NETWORKS:
|
||||
if ip in network:
|
||||
raise ValueError(f"URL resolves to blocked address: {ip}")
|
||||
|
||||
SKIP_EXT = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf", ".zip", ".mp3", ".mp4", ".css", ".js", ".ico", ".xml", ".json")
|
||||
|
||||
TRACKING_PARAMS = {
|
||||
|
|
@ -167,7 +199,8 @@ def get_site_name():
|
|||
|
||||
|
||||
def fetch_page(url):
|
||||
resp = requests.get(url, timeout=10, headers={"User-Agent": "TinyWeb/1.0"}, verify=False)
|
||||
_validate_url_target(url)
|
||||
resp = requests.get(url, timeout=10, headers={"User-Agent": "TinyWeb/1.0"})
|
||||
resp.raise_for_status()
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue