Authentication, Authorization & Security

Untrusted Data -- Injection & XSS


Learning Objectives

  • You can distinguish data from the code or syntax that interprets it.
  • You can prevent common SQL injection and unsafe HTML rendering patterns.
  • You can validate a public output and return only its parsed public fields.
  • You can explain why validation and TypeScript do not replace contextual defenses.

So far, we have checked who may read or change a note. Once we allow a request, we still need to decide how to use its data. What happens when a note title contains an apostrophe, or a profile biography contains HTML tags? Let’s follow those values through a database query, an API response and the browser.

Applications also use data in URLs, logs and configuration. A value safe in one context is not automatically safe in another. Injection occurs when untrusted data changes the meaning of a command or document instead of remaining data.

We will use notes and profiles to examine these boundaries through independent reading examples. The article and profile exercises supply their own files.

SQL values must not become SQL syntax

Start with a lookup by note title. For a notes table with id and title columns, let title be untrusted request text and sql a configured postgres.js client:

// Unsafe construction: do not execute this pattern.
const query = "SELECT id, title FROM notes WHERE title = '" + title + "'";

Concatenation makes title part of SQL syntax. The parameterized postgres.js version keeps it a value:

const rows = await sql`SELECT id, title FROM notes WHERE title = ${title}`;

Consider the title Reader's guide. In the concatenated query, its apostrophe becomes part of the SQL syntax. In the parameterized query, the complete title remains a value. Input validation does not replace parameterization.

Parameter placeholders cannot represent column names or sort keywords. For dynamic table or column names, select allowed identifiers and use the library’s identifier helper. For sort direction, choose ASC or DESC from an explicit allowlist. See the OWASP SQL Injection Prevention guidance.

Validate public response data

Our account examples have already selected the user fields that the browser may receive. Let’s apply that idea to a profile with a biography and an internal moderator note. Input validation checks a caller’s proposal. Output validation checks a value before the application returns it.

Output validation is especially useful when another application, an import job, a migration or an administrator can change stored data after our application has validated and saved it. An import might replace a valid biography with one longer than the public schema allows. Validation at the original write cannot guarantee that a later read still meets the response contract.

A changed repository query could also produce a string ID where a public response promises a number. Internal records may contain fields that must stay on the server.

After establishing that the caller may read the profile, we apply this public schema:

import { z } from "zod";
export const PublicProfileSchema = z.object({
id: z.number().int().positive(),
displayName: z.string().min(1).max(80),
biography: z.string().max(500),
});

This schema omits unknown properties from the parsed result. It neither converts a string ID to a number nor repairs an invalid display name. Zod explains stripping, rejecting, and retaining unknown object keys.

When the record matches the schema, parse returns a new public value; otherwise, it throws. Return the parsed value and leave the original record unchanged. At the HTTP boundary, treat an invalid output as an internal error without exposing the record or schema diagnostics.

Use output validation at a public boundary; it need not be repeated in every layer. Access checks still matter: a schema-valid record can belong to someone else. OWASP discusses public property selection under object property authorization.

Before reading the comments below, predict which fields parsing will return. Will it also remove the HTML tags from the biography or accept a string ID?

const record = {
id: 7,
displayName: "Example user",
biography: "<b>Just text</b>",
moderatorNote: "private",
};
const publicProfile = PublicProfileSchema.parse(record);
// { id: 7, displayName: "Example user", biography: "<b>Just text</b>" }
record.moderatorNote; // "private": parsing did not modify the original.
PublicProfileSchema.safeParse({ ...record, id: "7" }).success; // false

The parsed result omits moderatorNote, keeps the biography’s tags as string content, and requires a numeric ID. The original record still has its internal field. The article exercise transfers this pattern to a different public schema.

Figure 1 follows the biography across two boundaries: the schema checks and selects public fields; rendering turns the string into text or markup. Passing the schema does not make a string safe for raw HTML.

Figure 1. Output validation checks the public value; rendering controls how its text is interpreted.

Validate a public article output

0 / 15 points

Files in the editor

The editor below contains all 2 files for this exercise, although only one opens initially. Use the Files panel to expand the folders and select a filename. If the panel is collapsed, choose Show file tree in the editor toolbar.

  • Edit api/src/schemas/public-article-schema.ts.
  • Edit api/src/services/public-article.ts.

Complete both modules. The grader installs Zod and maps @src/ to api/src/.

Task

Complete api/src/schemas/public-article-schema.ts and api/src/services/public-article.ts. Export PublicArticleSchema and publicArticle(record: unknown) using the supplied paths.

This task starts after access has been authorized. An internal article may contain private fields. Its public output must contain exactly id, title, and summary: id is an integer from 1 through 2147483647, title is a string of 1-80 characters, and summary is a string of at most 500 characters. Do not coerce, trim, escape, or otherwise rewrite valid values. A string containing markup remains a string; rendering it safely is a separate responsibility.

Use a Zod object schema to check the proposed output and omit unknown fields. Return the parsed public value without mutating the input. If a required field is absent or invalid, throw an Error with message Invalid public article. Do not include the rejected record or schema details in that message.

Independent checks award 10 points for public fields and input preservation, and 5 for rejecting invalid output values.

Examples

{id:7,title:"Notes",summary:"<b>Text</b>",authorNotes:"private"} returns {id:7,title:"Notes",summary:"<b>Text</b>"} without changing the original. The same object with id:"7" must throw Error("Invalid public article").

Check and submit

Choose Submit solution below the editor to run the grading checks and read the feedback. All editor files, including unchanged files and files you have not opened, are submitted together. Keep file paths and exported names as specified.

Render user text as text

We now have a public biography, including any markup characters in its text. Let’s follow it into a Svelte component. Interpolations escape text:

<p>{biography}</p>

Raw HTML instead interprets the string as markup:

<!-- Do not use this for an untrusted biography. -->
{@html biography}

With <strong>not approved markup</strong>, interpolation displays literal characters while raw HTML creates an element. Untrusted raw HTML can also enable cross-site scripting (XSS): attacker-controlled code running in another user’s browser.

Compare the two outputs below. The first displays the tags as characters; the second uses them to make the words bold. This fixed HTML shows the difference:

<h2>Escaped text</h2>
<p>&lt;strong&gt;not approved markup&lt;/strong&gt;</p>
<h2>Rendered markup</h2>
<p><strong>not approved markup</strong></p>
When rich text is required

Text escaping, a form of output encoding, makes markup characters display as literal text. Sanitization removes disallowed markup while preserving an allowed subset, such as selected formatting elements. For rich text, use a maintained sanitizer configured to allow only the needed markup and test the rendering context. The OWASP XSS prevention guidance explains which defense applies in each context.

The same behavior applies to a small notice component. Before reading the explanation below, consider whether its paragraph will contain a strong element:

<script lang="ts">
const text = "<strong>Bring your notes</strong>";
</script>
<article>
<h2>Room notice</h2>
<p>{text}</p>
</article>

The paragraph has no strong child: the tags appear as text. This also holds when the string arrives through a prop or API.

Why does the raw-HTML conclusion fail?

0 / 5 points

A title is a TypeScript string, limited to 120 characters by Zod and stored with parameterized SQL. May it therefore be rendered as raw HTML?

For a profile that includes a website link, we need one more decision. HTML escaping does not restrict URL schemes, so a profile link also needs a scheme policy.

Parse a URL before rendering a link. The protocol property includes the scheme’s colon: new URL("https://example.test/help").protocol is "https:". Allow only "http:" and "https:"; omit links with other schemes, such as javascript:. A Zod URL schema can combine syntax checks with this protocol check.

Derive the parsed link from the current prop with Svelte’s $derived. Parsing only at component creation can leave a stale link after the prop changes. An invalid URL should remove the link; a later valid URL should restore it. The exercise supplies the URL schema.

If the server fetches a URL

A browser-link allowlist does not decide whether a server may fetch the URL. Server-side fetching needs a separate destination policy, including rules for access to internal network addresses.


Render public profile data in its proper context

0 / 25 points

Files in the editor

The editor below contains all 3 files for this exercise, although only one opens initially. Use the Files panel to expand the folders and select a filename. If the panel is collapsed, choose Show file tree in the editor toolbar.

  • Edit lib/components/Profile.svelte.
  • Keep unchanged lib/schemas/url-schema.ts.
  • Keep unchanged routes/+page.svelte.

Import the complete PublicHttpUrlSchema from #lib/schemas/url-schema.ts. The supplied application maps #lib/ to src/lib/. The browser checks use the supplied page controls to change the component props.

The grader supplies the Svelte 5/SvelteKit application, Zod, and browser checks. Editor paths are relative to that supplied application’s src/ directory; keep the shorter paths shown here in the editor.

Task

Complete lib/components/Profile.svelte with biography and homepage string props. Render an article headed Public profile, the biography as literal paragraph text, and a Visit homepage link only if new URL(homepage) succeeds and its protocol is http: or https:. Use the normalized URL as href. Invalid, relative, and other-scheme values produce no link. Output must follow changed props.

The browser checks cover literal text, permitted URLs, and removal of unsafe links when values change. A successful database read of an apostrophe-containing name would not establish that a component renders markup safely; this task assesses rendering and link policy directly.

Use the supplied Zod schema and its parsed output; its URL and HTTP/HTTPS checks are already implemented. This policy permits a browser link; it does not authorize a server-side fetch.

Suggested progression

Trace how your component responds when the checks change an accepted URL to a rejected scheme and then back to an accepted URL. Derive the parse from the current prop; a one-time parse misses these transitions.

Check and submit

Choose Submit solution below the editor to run the grading checks and read the feedback. All editor files, including unchanged files and files you have not opened, are submitted together. Keep file paths and exported names as specified.

Independent checks award 10 points for literal text, 5 for normalized HTTP/HTTPS links, and 10 for removing unsafe links when values change. Their feedback identifies the behavior to revise.

Validation, authorization, and safe rendering

Returning to our notes workflow, checking who may read a record is only one part of handling it. A valid string can still be unsafe HTML, and a parameterized query can still return someone else’s private record. Validation, safe rendering and authorization need separate checks.

Next, we will return to the browser’s session cookie and examine requests that another page might trigger while the user is signed in.

Check Your Understanding

  1. Why is validation not a substitute for parameterized SQL?
  2. Why should an API return the parsed public output rather than the original record?
  3. How does text interpolation differ from raw HTML insertion, even for a schema-valid biography?
  4. Why does a profile URL need its own scheme policy, and what should happen when its prop changes to a disallowed URL?