add abuse reporting and malicious link deletion

This commit is contained in:
sevi-py
2025-10-22 21:15:08 +02:00
parent 29d17daa1d
commit 9914a34d0d
4 changed files with 354 additions and 18 deletions

View File

@@ -19,5 +19,6 @@
"domain": {
"name": "your-domain.com",
"api_base_url": "https://your-domain.com"
}
},
"deletion_token": "EITHER_REPLACE_ME_OR_REMOVE_ME_TO_DISABLE_DELETION"
}

View File

@@ -8,6 +8,7 @@ from argon2.low_level import hash_secret_raw, Type
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
from hashlib import scrypt as hashlib_scrypt
# --- Determine absolute path for file access ---
APP_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -108,6 +109,126 @@ def decrypt_url(key, iv, ciphertext):
return plaintext.decode()
def hash_id_for_lookup_client(id_str):
"""Hash ID for lookup using the same method as the client (scrypt with LOOKUP_SALT)"""
LOOKUP_SALT = bytes([0x74, 0x6e, 0x79, 0x72, 0x2e, 0x6d, 0x65, 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x73])
# Use hashlib's scrypt with the same parameters as the frontend
# Frontend uses: N: 2**17, r: 8, p: 1, dkLen: 32
hash_result = hashlib_scrypt(
password=id_str.encode(),
salt=LOOKUP_SALT,
n=2**17,
r=8,
p=1,
dklen=32
)
return hash_result.hex()
def derive_encryption_key_client(id_str, salt):
"""Derive encryption key using the same method as the client (scrypt)"""
# Use hashlib's scrypt with the same parameters as the frontend
hash_result = hashlib_scrypt(
password=id_str.encode(),
salt=salt,
n=2**17,
r=8,
p=1,
dklen=32
)
return hash_result
def encrypt_url_client(key, plaintext):
"""Encrypt URL using the same method as the client (AES-CBC)"""
if len(key) != 32:
raise ValueError(f"Invalid key length: {len(key)} bytes (need 32)")
iv = os.urandom(16)
padder = padding.PKCS7(128).padder()
padded_data = padder.update(plaintext.encode()) + padder.finalize()
cipher = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
return iv, ciphertext
ABUSE_WARNING_MARKER = '__ABUSE_WARNING__'
@app.route('/delete-url', methods=['POST'])
def delete_url():
"""Replace a URL with an abuse warning page (both old and new encryption methods)"""
# Check if deletion is enabled
deletion_token = config.get('deletion_token', '')
if not deletion_token:
return jsonify({"error": "URL deletion is disabled"}), 403
data = request.get_json()
if not data or 'id' not in data or 'deletion_token' not in data:
return jsonify({"error": "Missing id or deletion_token"}), 400
# Verify deletion token
if data['deletion_token'] != deletion_token:
return jsonify({"error": "Invalid deletion token"}), 403
link_id = data['id']
updated = False
with get_db() as conn:
# Try to update old server-side encrypted URLs table
id_bytes = link_id.encode()
lookup_hash_old = derive_key(id_bytes, SALT1).hex()
# Check if it exists in old table
cur = conn.execute(
"SELECT 1 FROM urls WHERE lookup_hash = ?",
(lookup_hash_old,)
)
if cur.fetchone():
# Re-encrypt the abuse marker using server-side method
encryption_key = derive_key(id_bytes, SALT2)
iv, encrypted_warning = encrypt_url(encryption_key, ABUSE_WARNING_MARKER)
conn.execute(
"UPDATE urls SET iv = ?, encrypted_url = ? WHERE lookup_hash = ?",
(iv, encrypted_warning, lookup_hash_old)
)
conn.commit()
updated = True
# If not found, try to update new client-side encrypted URLs table
if not updated:
lookup_hash_new = hash_id_for_lookup_client(link_id)
# Check if it exists in new table
cur = conn.execute(
"SELECT 1 FROM client_side_urls WHERE lookup_hash = ?",
(lookup_hash_new,)
)
if cur.fetchone():
# Re-encrypt the abuse marker using client-side method
# Generate new encryption salt (since original was random)
new_encryption_salt = os.urandom(16)
encryption_key = derive_encryption_key_client(link_id, new_encryption_salt)
iv, encrypted_warning = encrypt_url_client(encryption_key, ABUSE_WARNING_MARKER)
conn.execute(
"UPDATE client_side_urls SET encryption_salt = ?, iv = ?, encrypted_url = ? WHERE lookup_hash = ?",
(new_encryption_salt, iv, encrypted_warning, lookup_hash_new)
)
conn.commit()
updated = True
if updated:
return jsonify({"message": "URL replaced with abuse warning successfully"}), 200
else:
return jsonify({"error": "Link not found"}), 404
@app.route('/shorten-server', methods=['POST'])
def shorten_url_server():
data = request.get_json()
@@ -239,6 +360,122 @@ def redirect_url(id):
except Exception as e:
return jsonify({"error": "Decryption failed"}), 500
# Check if this is an abuse warning
if url == ABUSE_WARNING_MARKER:
# Redirect to home page with abuse warning hash
domain = config.get('domain', {}).get('name', 'tnyr.me')
abuse_html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<title>Link Removed - Abuse Detected</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #9333ea 0%, #7e22ce 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 20px;
}}
.container {{
background: white;
border-radius: 12px;
padding: 2rem 3rem;
max-width: 700px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}}
h1 {{
color: #dc2626;
margin-top: 0;
font-size: 28px;
}}
.warning-icon {{
font-size: 64px;
text-align: center;
margin-bottom: 20px;
}}
p {{
color: #374151;
line-height: 1.6;
margin: 15px 0;
}}
.alert-box {{
background: #fef2f2;
border-left: 4px solid #dc2626;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}}
.info-box {{
background: #eff6ff;
border-left: 4px solid #2563eb;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}}
ul {{
color: #374151;
line-height: 1.8;
}}
li {{
margin: 8px 0;
}}
strong {{
color: #1f2937;
}}
a {{
color: #2563eb;
text-decoration: underline;
}}
a:hover {{
color: #1d4ed8;
}}
</style>
</head>
<body>
<div class="container">
<div class="warning-icon">⚠️</div>
<h1>This Link Has Been Removed</h1>
<div class="alert-box">
<p><strong>This shortened URL has been disabled due to abuse reports.</strong></p>
</div>
<p>The link you followed has been removed from our service because it was reported for one or more of the following reasons:</p>
<ul>
<li>Phishing or scam attempt</li>
<li>Malware distribution</li>
<li>Fraudulent content</li>
<li>Harassment or threats</li>
<li>Other malicious activity</li>
</ul>
<div class="info-box">
<p><strong>⚠️ Important Security Reminders:</strong></p>
<ul>
<li>Never share personal information, passwords, or financial details through untrusted links</li>
<li>Be cautious of urgent messages claiming your account will be locked or money is owed</li>
<li>Verify the authenticity of communications by contacting organizations directly through official channels</li>
<li>Legitimate companies will never ask for sensitive information via email or text messages</li>
<li>If something seems too good to be true, it probably is</li>
</ul>
</div>
<p style="text-align: center; font-size: 14px;">
<strong>If you believe this link was removed in error, please contact us at <a href="mailto:abuse@{domain}">abuse@{domain}</a></strong>
</p>
</div>
</body>
</html>"""
return abuse_html, 200
return redirect(url, code=302)
@app.route('/')

View File

@@ -8,6 +8,7 @@ import { Input } from "./components/ui/input";
import { Button } from "./components/ui/button";
import { Shield, Key, Hash, Lock, Copy, EyeOff, Github, Loader2 } from "lucide-react";
import { SiBuymeacoffee } from "@icons-pack/react-simple-icons";
import AbuseWarning from "./components/AbuseWarning";
const urlSchema = z.object({
url: z
@@ -129,6 +130,7 @@ export default function App() {
const [shortened, setShortened] = useState("");
const [loading, setLoading] = useState(false);
const [isDecrypting, setIsDecrypting] = useState(false);
const [showAbuseWarning, setShowAbuseWarning] = useState(false);
// Check for hash in URL on component mount for decryption
useEffect(() => {
@@ -161,6 +163,13 @@ export default function App() {
const encryptedUrl = hexToArray(ENCRYPTED_URL);
const decryptedUrl = await decryptUrl(decryptionKey, iv, encryptedUrl);
// Check if this is an abuse warning
if (decryptedUrl === '__ABUSE_WARNING__') {
setIsDecrypting(false);
setShowAbuseWarning(true);
return;
}
// Redirect to decrypted URL
window.location.href = decryptedUrl;
} catch (error) {
@@ -236,6 +245,11 @@ export default function App() {
navigator.clipboard.writeText(shortened);
};
// Show abuse warning if detected
if (showAbuseWarning) {
return <AbuseWarning domain={DOMAIN} />;
}
// Decryption loading screen
if (isDecrypting) {
return (
@@ -408,23 +422,34 @@ export default function App() {
</div>
</div>
<footer className="bottom-4 flex items-center gap-3 text-slate-400">
<a
href="https://github.com/Sevi-py/tnyr.me"
target="_blank"
rel="noopener noreferrer"
className="hover:text-slate-300 transition-colors"
>
<Github className="w-8 h-8" />
</a>
<a
href="https://www.buymeacoffee.com/severin.hilbert"
target="_blank"
rel="noopener noreferrer"
className="hover:text-slate-300 transition-colors"
>
<SiBuymeacoffee className="w-8 h-8" />
</a>
<footer className="bottom-4 flex flex-col items-center gap-3">
<div className="flex items-center gap-3 text-slate-400">
<a
href="https://github.com/Sevi-py/tnyr.me"
target="_blank"
rel="noopener noreferrer"
className="hover:text-slate-300 transition-colors"
>
<Github className="w-8 h-8" />
</a>
<a
href="https://www.buymeacoffee.com/severin.hilbert"
target="_blank"
rel="noopener noreferrer"
className="hover:text-slate-300 transition-colors"
>
<SiBuymeacoffee className="w-8 h-8" />
</a>
</div>
<div className="text-xs text-slate-500 text-center">
Report abuse:{" "}
<a
href={`mailto:abuse@${DOMAIN}`}
className="hover:text-slate-400 transition-colors underline"
>
abuse@{DOMAIN}
</a>
</div>
</footer>
</div>
);

View File

@@ -0,0 +1,73 @@
import { useEffect } from 'react';
interface AbuseWarningProps {
domain?: string;
}
export default function AbuseWarning({ domain = 'tnyr.me' }: AbuseWarningProps) {
useEffect(() => {
// Add meta tags to prevent indexing
const metaRobots = document.createElement('meta');
metaRobots.name = 'robots';
metaRobots.content = 'noindex, nofollow';
const metaGooglebot = document.createElement('meta');
metaGooglebot.name = 'googlebot';
metaGooglebot.content = 'noindex, nofollow';
document.head.appendChild(metaRobots);
document.head.appendChild(metaGooglebot);
// Cleanup on unmount
return () => {
document.head.removeChild(metaRobots);
document.head.removeChild(metaGooglebot);
};
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-purple-600 to-purple-800 text-slate-100 flex items-center justify-center p-4">
<div className="bg-white text-slate-900 rounded-xl p-8 md:p-12 max-w-2xl shadow-2xl">
<div className="text-center text-6xl mb-6"></div>
<h1 className="text-3xl font-bold text-red-600 mb-6">This Link Has Been Removed</h1>
<div className="bg-red-50 border-l-4 border-red-600 p-4 mb-6 rounded">
<p className="font-semibold text-red-900">
This shortened URL has been disabled due to abuse reports.
</p>
</div>
<p className="mb-4 text-slate-700">
The link you followed has been removed from our service because it was reported for one or more of the following reasons:
</p>
<ul className="list-disc list-inside mb-6 space-y-2 text-slate-700">
<li>Phishing or scam attempt</li>
<li>Malware distribution</li>
<li>Fraudulent content</li>
<li>Harassment or threats</li>
<li>Other malicious activity</li>
</ul>
<div className="bg-blue-50 border-l-4 border-blue-600 p-4 mb-6 rounded">
<p className="font-semibold text-blue-900 mb-3"> Important Security Reminders:</p>
<ul className="list-disc list-inside space-y-2 text-sm text-slate-700">
<li>Never share personal information, passwords, or financial details through untrusted links</li>
<li>Be cautious of urgent messages claiming your account will be locked or money is owed</li>
<li>Verify the authenticity of communications by contacting organizations directly through official channels</li>
<li>Legitimate companies will never ask for sensitive information via email or text messages</li>
<li>If something seems too good to be true, it probably is</li>
</ul>
</div>
<p className="text-sm text-slate-600 text-center">
<strong>If you have further questions, please contact us at{' '}
<a href={`mailto:abuse@${domain}`} className="text-blue-600 hover:text-blue-700 underline">
abuse@{domain}
</a></strong>
</p>
</div>
</div>
);
}