Lesson 9 / الدرس 9

Text that turns into markup / نصٌّ يتحول إلى وسوم

Printing what a visitor typed straight into a page hands them control of that page — for everyone who reads it afterwards, not just for themselves. One function fixes it, and the discipline is applying it every single time.

طباعة ما كتبه زائر مباشرةً في صفحة تسلّمه التحكم في تلك الصفحة — لكل من يقرؤها بعده، لا له وحده. ودالة واحدة تصلح ذلك، والانضباط هو تطبيقها في كل مرة دون استثناء.

A browser cannot tell the difference between markup you wrote and markup that arrived in a comment box. It parses both. So the moment a visitor's text is printed into a page unchanged, whatever tags they included become part of that page — and the people who suffer are the next visitors, who never typed anything.

Watch a comment stop being a comment

<h1>Comments</h1>

<!-- Sara typed: Great lesson, thank you -->
<p class="comment"><b>Sara:</b> Great lesson, thank you</p>

<!-- Someone typed the line below into the same box. -->
<!-- The server printed it without changing anything.  -->
<p class="comment"><b>Guest:</b> Nice work</p>
<h2 style="color:#b3261e">SITE CLOSED — email us your password</h2>

<p style="margin-top:24px">Everything below the heading is still
the comment. It was text when it was typed and it is markup now.</p>
Run it. The second comment has taken over the page, and nothing here is a bug in the browser — it did exactly what markup says. If a heading works, a <script> works, and a script in someone else's page can read their session and act as them. This site's Content-Security-Policy stops the script half of that demonstration, which is a second wall, not the first one.

Escape on the way out, every time

<?php
// This site's helper, in src/helpers.php. Yours should look the same.
function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

//   ENT_QUOTES     escape ' as well as " — without it, a value inside
//                  a single-quoted attribute can break out of it
//   ENT_SUBSTITUTE invalid UTF-8 becomes a replacement character rather
//                  than an empty string, which is how a bad byte can
//                  silently delete the rest of a value
?>

<p><b><?= e($comment['author']) ?>:</b> <?= e($comment['body']) ?></p>
<a href="/user/<?= e($comment['author_id']) ?>">Profile</a>
<img src="<?= e($avatar) ?>" alt="<?= e($comment['author']) ?>">
Escaping belongs at the moment of printing, not when the value is saved. Store exactly what the person typed — if you escape on the way in, the database fills up with &amp; and the same value is wrong the moment you need it somewhere that is not HTML: an email, a CSV, a JSON response.
Where the value landsWhat escaping it needs
Between tags, or in an attributee() — htmlspecialchars
Inside a href or srce(), and check the scheme is http/https first
Inside a <script> blockjson_encode — HTML escaping is the wrong language here
Inside a CSS valueDo not. Pick from a list of values you wrote
Into an SQL queryNothing — use a placeholder, which the next chapter covers

The href row catches people out: e() handles the quotes but not the scheme, and javascript:alert(1) contains no character htmlspecialchars touches. Escaping answers "can this break out of where I put it", not "is this safe" — a URL needs both questions asked.

Try it live / جرّب بنفسك

Preview / المعاينة

Check yourself / اختبر نفسك

1. Who is harmed when a site prints a comment without escaping it?

2. Why escape when printing rather than when saving?

3. Why is e($url) not enough for an href?

Your task / مهمتك

Take a page that shows something a visitor wrote — a comment list, a profile, a search results page. Mark every place a value is printed, say where that value came from, and say what escaping it needs for the place it lands in.

خذ صفحة تعرض شيئًا كتبه زائر — قائمة تعليقات أو ملف تعريف أو صفحة نتائج بحث. علّم كل موضع تُطبع فيه قيمة، وقل من أين أتت تلك القيمة، وقل ما التهريب الذي تحتاجه للموضع الذي تحطّ فيه.

  • Every printed value marked, with where it came from كل قيمة مطبوعة معلَّمة، ومن أين أتت
  • For each one, the escaping its destination needs — between tags, in an attribute, in a URL لكلٍّ، التهريب الذي تحتاجه وجهتها — بين وسوم، أو في سمة، أو في رابط
  • One value you would have trusted, and the form that could set it قيمة واحدة كنت ستثق بها، والنموذج الذي يستطيع ضبطها
How do you want to submit? / كيف تريد التسليم؟