Technical SEO for Modern Web Apps: A Code-Level Checklist
Core Web Vitals, canonical and hreflang, rendering strategy and structured data: what to check in your code so search engines understand your site correctly.
Prepared statements, context-aware output escaping, Content Security Policy and safe error handling: practical defences against the two most common attacks on PHP projects.
Injection attacks have sat near the top of the OWASP Top 10 for years. The good news: most SQL injection and XSS vulnerabilities can be eliminated with a few habits set up correctly from the start. This article summarises the core defences I use in PHP projects, with code examples.
The problem is concatenating user input into SQL:
// WRONG: typing ' OR '1'='1 into the email field changes the query
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
The fix is prepared statements. The query and the data are sent to the database separately, so data is never interpreted as a command:
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
Table names, column names and sort direction can't be bound as parameters. If these come from the user, accept only one of a predefined set of values:
$allowed = ['created_at', 'price', 'title'];
$column = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'created_at';
$stmt = $pdo->query("SELECT * FROM listings ORDER BY $column DESC");
Also give the application's database user only the privileges it needs; a web app has no reason to be able to drop tables.
XSS happens when content entered by a user runs as HTML or JavaScript on the page. The core rule: escape data not when you store it but when you output it, and in a way that fits the context.
function e(mixed $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>
<h2><?= e($listing['title']) ?></h2>
<a href="/listing/<?= e(rawurlencode($listing['slug'])) ?>">Details</a>
ENT_QUOTES escapes single and double quotes too, which is essential inside attributes.http and https schemes; javascript: URLs still run even when escaped.json_encode with the JSON_HEX_TAG and JSON_HEX_AMP flags.You can't escape fields where HTML is allowed, such as a blog editor; use a sanitiser that allowlists permitted tags and attributes instead (for example HTML Purifier). script, on* event attributes and style should never get through.
Even if escaping is forgotten somewhere, CSP makes the browser run scripts only from sources you allow. For a site without inline scripts, this header is a strong starting point:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
base-uri 'self'; frame-ancestors 'none'; form-action 'self'
Marking session cookies HttpOnly, Secure and SameSite=Lax also makes it harder for a potential XSS to turn into session theft.
display_errors off in production; write detailed errors to the server log and show users only a generic message. Directory paths and table names must never leak..env file outside the web root.composer audit lists known vulnerabilities.Prepared statements and allowlists for SQL; context-aware escaping and a sanitiser for HTML; CSP and secure cookies for the browser. Applied together, these three layers stop most common attacks at the door. Security isn't a feature you add later; it's a way of writing code from the first line.
Core Web Vitals, canonical and hreflang, rendering strategy and structured data: what to check in your code so search engines understand your site correctly.
Choosing the right software depends on cost, data ownership, integrations and your growth plans. Seven questions to answer before you commit.