Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5) Afternoon Question 1 Explained — The Stored XSS Where 16 Reviews Show Up as 2

· Updated: · · Registered Information Security Specialist, Registered Security Specialist, XSS, Cross-Site Scripting, Web Application, Information Security, Vulnerability, IPA, Session Management

Revision history (first version, published Jul 29, 2026)
First published
Cite this article(DOI (registered archive): 10.5281/zenodo.22170800)

The DOIs below refer to previously archived versions and may not match the current text. Use this page’s URL to reference the current text.

Go Komura (2026). Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5) Afternoon Question 1 Explained — The Stored XSS Where 16 Reviews Show Up as 2. KomuraSoft LLC. https://comcomponent.com/en/blog/sc-exam-r5a-pm-q1-stored-xss/

DOI (registered archive)
10.5281/zenodo.22170800
DOI (last registered version)
10.5281/zenodo.22170801

“There are supposed to be 16 reviews, but only 2 are displayed.”

Afternoon Question 1 of the Autumn 2023 (Reiwa 5) Registered Information Security Specialist Examination begins with this inquiry from a user1. There is nothing odd about the screen transitions, and no error appears. It is only that the displayed count does not add up.

The cause was stored cross-site scripting (XSS). What makes this question interesting, though, is not the answer “XSS” itself. It is that every plausible-looking countermeasure Company Q, the e-commerce operator in the problem statement, already had in place was slipped past. The 50-character limit on the review title was broken, the token required for the upload was obtained through the legitimate procedure, and the stolen session ID was carried out without ever being sent to an external server.

This article reads the case in the order “what happened, then which traces form the basis for the answer, then which countermeasure could have stopped it.” Beyond the name XSS, it sorts out where the line falls between the countermeasures that did not work and the ones that would have.

It covers the exam’s model answers and the basis for each, and a complete picture of XSS countermeasures you can use in practice. Choose the entry point that matches your purpose.

Why you are reading Where to start How to read on
You want to work the question again The question map in Section 2 Open the question booklet and check the model answers and their basis in Sections 4 to 8
You want to know why 16 reviews look like 2 The symptom in Section 3 Grasp the difference between the display and the HTML, then move on to the split posts in Section 5
You want to see the whole route by which information is carried out The sequence diagram in Section 7 Follow the posting, the execution on the victim’s side and the retrieval on the attacker’s side separately
You want to use this for countermeasures or reviews at work The countermeasure comparison in Section 9, the checklist in Section 10 Separate fundamental fixes from fallback countermeasures, and check the scope and the limits of each

In Sections 4 to 8 the model answers are the exam’s answers, while CSP, image re-encoding and the like are supplementary material that extends into practice. Section 8 in particular separates the conditions written in the problem statement from the conditions that apply once a setting is changed.

For the wider question of what criteria to use when checking the security of a website as a whole, see What Website Clients Should Know Too — Using IPA’s ‘How to Secure Your Website’ as a Checklist. This article takes one of the 11 vulnerabilities listed there, XSS, and digs into it with a worked example.

1. The Bottom Line First

What Happened

  • The type of vulnerability is stored XSS. The attacker’s string was saved on the server and, from then on, output into the HTML of everyone who opened the page. Whether the embedded script uses DOM APIs and whether the case is DOM Based XSS are two different matters
  • The input character limit did not work as an XSS countermeasure. The attacker split the payload across 15 posts and had the HTML that falls between posts skipped over as a JavaScript comment, joining the fragments into a single script
  • The upload token (a CSRF countermeasure) did not work as an XSS countermeasure either. The attack script obtains the token through the same procedure as the legitimate screen. As long as it runs on the same origin, it can do everything a legitimate user can do
  • The stolen session ID was never sent outside. The contents of the cookie were placed on the site’s own avatar upload feature as an image file named “a.png”, and the attacker retrieved it by simply viewing it. Egress controls cannot detect this

Where It Could Have Been Stopped

  • The problem statement explicitly names three things Company Q lacked: escaping at output time (the fundamental fix), and the HttpOnly attribute on the cookie and checking the format of uploaded files (these two are fallback countermeasures). Any one of them would have broken this attack chain. Format checking alone, however, can be slipped past if the technique is changed, so re-encoding is needed as well
  • Although it does not appear in the problem statement, CSP’s script-src can break the same chain. With a configuration that does not allow 'unsafe-inline', the embedded inline script would never run in the first place

“Works” here means being able to stop this question’s attack chain. HttpOnly or image re-encoding on its own does not make XSS itself go away. Section 9 also looks at the limits that appear once the technique changes.

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (25 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

2. About the Source Material — Attribution and How This Article Handles It

The question examined here is the following.

Source: Autumn 2023 (Reiwa 5) Registered Information Security Specialist Examination, Afternoon, Question 1

IPA states that, for the past exam questions it publishes, no permission or usage fee is required except where the law specifically provides otherwise. That does not mean IPA has waived copyright: it requires the source to be stated in the form “fiscal year, session, examination category, time slot, question number, and so on”, and requires that any partial modification of a question be noted as well2.

This article does not reproduce the HTML or scripts printed in the question booklet. Where the explanation of the mechanism requires it, they are replaced with equivalent sample code we wrote ourselves. The question text and the model answers are also handled in summarized form. The original question booklet, model answers and grading commentary can be downloaded free of charge from IPA’s page, so we recommend reading this article with them open at hand1 3 4.

How the Sub-Questions Map to This Article

For readers who have the question booklet open, here is how the sub-questions map to the sections of this article. Feel free to start from the sub-question you want to work on.

Sub-question What is asked (character limit) Corresponding section of this article
Question 1(1) The type of XSS vulnerability that was exploited (three choices) Section 4
Question 1(2) The countermeasure for web app Q (30 characters or fewer) Section 4, “Question 1(2): The Countermeasure”
Question 2 The method used to get a script longer than the input character limit to run (50 characters or fewer) Section 5
Question 3(1) What lines 6 to 20 of the attack script do (60 characters or fewer) Section 6
Question 3(2) How the attacker retrieves the uploaded information (50 characters or fewer) Section 7
Question 3(3) What can be done with the retrieved information (40 characters or fewer) Section 7, “Question 3(3): What Can Be Done With the Session ID”
Question 4 The browser mechanism that keeps the attack from succeeding on the attacker’s own domain (40 characters or fewer) Section 8

If you want only the practical material and not the sub-questions, read Section 9 (the list of countermeasures that worked and did not work) and Section 10 (code-review angles). If you want to see the attack chain itself on a single page, the sequence diagram in Section 7 gives the whole picture from posting to retrieval.

How the Question Booklet’s Text Maps to the Samples in This Article

So that you can compare with the original, here is a summary of what was replaced and how.

Text in the question booklet How this article handles it Where it appears
The HTML of page V (including the review titles that were posted in split form) Not reproduced; we wrote an equivalent sample that shrinks the same mechanism down to three posts Section 5
The extracted attack script (about 20 lines) Not reproduced; we wrote equivalent JavaScript that performs the same processing. The comments in the code are for this article’s explanation Section 6
The text of each sub-question Summarized while keeping the gist (conditions such as character limits use the original values) The start of each of Sections 4 to 8
Model answers The model answers IPA publishes3 Throughout Sections 4 to 8
Grading commentary The relevant passages of the grading commentary IPA publishes4 Sections 4, 5 and 7
The specifications in the problem statement (Company Q, page V, members A and B, the features and character limits, and so on) Summarized in line with the original text Section 2, “The Setting of the Question”

The Setting of the Question

The company involved is Company Q, a clothing e-commerce business with 100 employees. It runs its online store on web app Q, which it developed in house, and users access it over HTTPS. The premise is that it has just added a product review feature for members.

There are five specifications to keep in mind.

Feature Specification
Login Authenticates with a member ID and password and issues a session ID as a cookie
Product review Only logged-in members can post. The review title has an input character limit of 50 characters and the review body one of 300 characters, and both are free-form text
Member profile Provides a page for uploading an avatar image and a page for registering credit card information. Both are available only to logged-in members
Avatar image upload Sends the image file and a token as parameters to /user/upload. It succeeds only when the token matches the one issued at /user/profile
Avatar image display The uploaded avatar image is displayed on the member profile settings page and on review pages

The last two rows are the clues for thinking about “what is needed in order to upload” and “who can read it once it is uploaded.” Obtaining the token is covered in Section 6, and retrieval through the image in Section 7.

3. The Symptom — Why 16 Reviews Became 2

Start by Comparing the Count on Screen With the Count in the HTML

An inquiry comes in from a member: on the review page for the plain T-shirt (page V), only 2 of the 16 reviews that should be displayed are showing. When Mr. N of the development department opens page V, the screen looks like this.

  • The header says “16 reviews”
  • One review from member A (title “Good”, body “Nice shirt!”)
  • One review from member B
  • At the end, “That is all 16 reviews”

The count displayed is 16, but only 2 are actually laid out. Checking the HTML at this point reveals 15 posts by member A, with a long script embedded among them.

In other words, all 16 reviews really are output into the HTML. The reason only 2 are visible anyway is that most of them ended up as the contents of a <script> element, so the browser stopped treating them as content to be displayed.

What Stays Visible Is the First Half of Post 1 and the Second Half of Post 15

The range that gets swallowed is not, strictly speaking, “all 15 posts.” The opening tag <script> appears partway through the first review title (right after “Good”), and the closing tag </script> sits at the end of the fifteenth review title. Therefore:

  • The first post’s card (avatar, display name, date, stars, and the title as far as “Good”) sits before <script>, so it is displayed
  • Everything from there to the end of the fifteenth title is swallowed as the contents of the script element
  • The rest of the fifteenth post (the body text “Nice shirt!”) sits after </script>, so it is displayed

The result is that the first post’s header section joins up with the fifteenth post’s body, and on screen it looks like “one review by member A with the title Good and the body Nice shirt!” Add member B’s single review and you get 2. The attacker presumably placed natural-looking strings, “Good” at the start of the first post and “Nice shirt!” in the body of the fifteenth, so that the broken layout would not look unnatural.

Knowing how to read this “the count is right but the display is not” symptom is useful in practice too. It is the entry point for suspecting that, rather than a bug in the display-count logic, the structure of the output HTML has been broken.

4. Why This Is “Stored” XSS — Question 1

Question 1(1): The Answer, and the Traces Used to Reach It

Question 1 asks you to choose the type of XSS vulnerability used in this attack from three options: DOM Based XSS, stored XSS and reflected XSS. The correct answer is stored XSS.3

The basis is that the posted string was saved on the server and output into the HTML as-is. Do not choose the type from the names of the APIs the script uses once it is running.

IPA’s grading commentary says the following about this sub-question.4

The percentage of correct answers was average, but a number of candidates were seen to answer incorrectly with “DOM Based XSS”, apparently because the script used the DOM.

The attack script that appears in the problem statement uses XMLHttpRequest, receives the response as a DOM, and looks up an element with getElementById. It does indeed touch the DOM. But that has nothing to do with the type of vulnerability.

The Dividing Line Is Where the Attack String Becomes a Script

What separates the three categories is where the attack string turns into executable code, that is, whether the vulnerable output point (the sink) is on the server side or the browser side.

Type Sink (where the attack string becomes executable) Where the attack string comes from
Reflected XSS The server’s HTML assembly processing The request itself, such as a parameter in a URL the attacker prepared
Stored XSS The server’s HTML assembly processing Data saved on the server side
DOM Based XSS JavaScript in the browser (assignment to innerHTML, eval, document.write and so on) A URL fragment, postMessage, a value received from the server, and so on

In this question, the server embedded the review title the attacker posted into the HTML as-is and returned it. The sink is in the server-side output processing, and the string is saved in the database and then handed to everyone who opens page V from then on. So it is stored XSS.

When JS in the browserpassed it to innerHTMLor similarIt was in the HTMLthe server assembledSaved(output to everyone from then on)Not saved(only for that request)The attacker's string ranas a scriptWhere did it becomeexecutableDOM Based XSSIs that string savedon the serverStored XSSReflected XSS

Simplifying this into “if the attack string is in the server’s response, it is stored or reflected” is dangerous. Even when the server returns the value as harmless text or JSON, it becomes DOM Based XSS if JavaScript in the browser assigns it to innerHTML (this is so-called stored DOM XSS, where a saved value is the cause). In that case what has to be fixed is not the server’s output processing but the client-side sink, so judge by where the value became executable, not by whether the string is visible in the response.

What the embedded script does (touch the DOM, communicate, read cookies) and how that script got into the page have to be thought about separately. Confusing the two matters not because it makes choosing a countermeasure awkward, but because it makes you choose the wrong one. Deciding the case is DOM Based XSS leads to the conclusion “we just need to fix the client-side JavaScript,” whereas what actually needed fixing was the server-side output processing.

Question 1(2): The Countermeasure

Question 1(2) asks for the countermeasure in web app Q in 30 characters or fewer.

Model answer: “Apply escaping to the review title before output.”3

Basis for the answer: The place to fix is not the screen that accepts the post, but the processing that embeds the stored review title into the HTML.

The fact that the ordering, “before output,” is spelled out here is important. You do not escape when the input is received; you escape immediately before output as HTML, according to the context of the destination. IPA’s “How to Secure Your Website” likewise puts “apply escaping to every element output to the web page” at the head of its fundamental fixes for XSS5.

“Isn’t Escaping Input What Matters for SQL Injection?”

From here on is supplementary material that extends the “before output” of Question 1(2) into practice. Read it as separate from the model answer itself.

To put the conclusion first: for SQL injection too, “escaping on input” is not the countermeasure.

What IPA lists as the fundamental fix for SQL injection is to “implement all SQL statement construction with placeholders.” Escaping is listed as an alternative for when a SQL statement has to be built by string concatenation, but even there the wording is “when the SQL statement is assembled by string concatenation, use the database engine’s API that performs escaping and the like to correctly form the literals of the SQL statement,” so escaping happens at the moment the SQL statement is formed6. Not at the point where the input is received.

The structure is therefore exactly the same as for HTML. The shared principle comes out like this.

Escaping is performed where it is decided which grammatical world the data is heading out into.

  • If it goes out as HTML, HTML escaping
  • If it becomes part of a SQL statement, placeholders (or, where unavoidable, escaping as a SQL literal)
  • If it becomes part of a shell command, processing in the shell’s own conventions

Process It Where the Destination Is Decided, Not on Input

Why not on input? Because at the point where the input is received, it is not yet decided where that data will go. The same review body goes out to an HTML page, to a CSV export, to a JSON API response, to a notification email and to a log. Apply HTML escaping on input and the string &amp; shows up verbatim in the CSV, while the database holds something different from the original input, throwing off both search and aggregation. It is a breeding ground for double escaping as well.

So Does the Input Side Need to Do Nothing?

No. What the input side does, though, is validation, not escaping. The roles are different.

  • Reject on input — do not accept values that are impossible under the specification. A postal code field rejects anything but 7 digits; a quantity field rejects negative numbers. This is processing needed in its own right, to protect the correctness of the data
  • Escape on output — represent the accepted data safely in the grammar of wherever it is going. This is the fundamental fix for the vulnerability

It also matters not to expect too much of input-side validation as an XSS countermeasure. For XSS, IPA touches on the method of checking whether input values conform to the application’s specification and then states explicitly that the effectiveness of this countermeasure is limited, that it is no countermeasure at all where the specification the application requires permits input over a wide range of character types, and that relying on this method is therefore not recommended5.

The review title and review body here were exactly that: free-form fields whose specification permits a wide range of character types. There are upper limits of 50 and 300 characters, but the number of characters needed to make a script run is far smaller than that, so having an upper limit is not in itself a defense. What actually happened at Company Q is what Question 2, coming up next, looks at.

5. How the 50-Character Limit Was Broken — Question 2

Question 2 is the highlight of this question set.

Regarding Figure 3, describe in 50 characters or fewer the method used to make a script longer than the input character limit run.

Model answer:The attacker made the posts in several parts, such that the HTML would be commented out and become a single script.3

The answer has to cover not only the multiple posts but also the mechanism that skips the intervening HTML as a comment. Let us check it in the following three stages.

Look at the Posts, the HTML and the JavaScript in That Order

The attacker split the payload into lengths that fit into a single post and posted 15 times. The key point is what to do about the HTML that inevitably comes between one post and the next (</div>, <div class="..."> and so on). As JavaScript, those are syntax errors.

What was used to handle that is the JavaScript block comment. The following is a simplified version we wrote ourselves to explain the mechanism (it is not a verbatim quotation of a figure from the question booklet).

1. The Strings That Are Posted

Suppose the review title field is posted in three parts, like this.

Post 1: Wonderful<script>a=1;/*
Post 2: */b=2;/*
Post 3: */c=3;</script>

2. The HTML the Server Assembles

The server outputs these as the title of each review, producing HTML like this.

<div class="review-title">Wonderful<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>

3. The JavaScript the Browser Runs

From the browser’s point of view, everything from the first <script> to the last </script> is a single script element. Its contents come out as follows.

a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;

The HTML sandwiched between /* and */ is skipped over as a JavaScript comment, and all that actually runs is a=1; b=2; c=3;. And because the part skipped over as a comment is still inside the script element, it is not displayed on screen either. That is what the “16 reviews look like 2” symptom really is.

What to Take Away From This

Do not get the causality backwards here. An input character limit fails as an XSS countermeasure not because the number of posts was unlimited, but because 50 characters is already enough to run a script. Adding a single event handler attribute, or placing a single tag that loads an external script, fits into a few dozen characters either way. Even with posting limited to one, a character limit would never amount to a defense.

What, then, were the split posts in this case? They were the means of bringing in a long script of some 20 lines in its entirety. What the attacker wanted to do was long, so they made up for it with repetition. That is not in itself the reason the character limit was broken. Describing the technique and assessing it as a countermeasure have to be kept apart.

The same applies to input-side restrictions in general.

  • Character limits, restrictions on which character types may be entered, front-end validation — these are required by the specification, but they are no substitute for the fundamental fix for XSS
  • Attackers always have room to work around input-side restrictions by splitting the payload, encoding it, or injecting it through a different route
  • The place to defend is the moment the data goes out as HTML

Grading Commentary: Do Not Assume the Limit Was Removed

IPA’s grading commentary also says of this sub-question that “some answers appeared to reflect insufficient checking, such as ‘the attacker deleted the input restriction with the developer tools and then posted.’” It is true that front-end restrictions can be removed with the developer tools. What this sub-question asked for, however, was to read what was actually done out of the traces left in the HTML. The HTML in Figure 3 held 15 separate posts, each short enough to fit within the limit, with comment markers placed at the start and end of each. Those are traces of the limit being worked around, not of its being removed. It should be read as a question about the discipline of checking, one by one, what the attacker left behind.

The importance of reading the evidence this way is pointed out in the grading commentary as well.4

The design thinking behind how far and in what way to validate input values is also laid out, as a design that does not trust data coming in from outside, in Never Use a QR Code’s Decoded Value As-Is — Error Correction Succeeding Does Not Guarantee the Value.

6. What the Script Did — Question 3(1)

The script Mr. N extracted is about 20 lines long. Question 3(1) asks, in 60 characters or fewer, what lines 6 to 20 of it do, that is, the part that runs once the first request has succeeded.

Model answer:It uploads the session ID as an avatar image, together with the token obtained from the XHR response.3

The reading order is obtain the token, turn the cookie string into a file, upload it with the token attached. Separating out what was sent, as what, and to where brings the key points of the answer into view.

Below is equivalent code we wrote ourselves to explain this behavior (it is not a verbatim quotation of a figure from the question booklet). The “lines 6 to 20” in the sub-question are line numbers in the question booklet, not in the sample below. In the sample, comment (1) is the preparation, and comments (2) to (4) correspond to the processing the sub-question asks about.

// (1) First, fetch the profile settings page
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document";   // receive the response as a DOM rather than as text
xhr.send();

xhr.onload = function () {
  // (2) read the upload token the same way the legitimate screen does
  const token = xhr.response.getElementById("token").value;

  // (3) make the cookie (which contains the session ID) the contents of a PNG file as-is
  const file = new File([document.cookie], "a.png", { type: "image/png" });

  // (4) send it to the site's own avatar image upload feature
  const form = new FormData();
  form.append("uploadfile", file);
  form.append("token", token);

  const xhr2 = new XMLHttpRequest();
  xhr2.open("post", "https://example.jp/user/upload");
  xhr2.send(form);
};

The First Five Lines Exist Only to Get Past Company Q’s CSRF Countermeasure

What deserves attention in this script is that the whole first half is spent obtaining the token.

For avatar image uploads, Company Q required that the token match the one issued at /user/profile. That is a mechanism for preventing an unauthorized upload from a fake form placed on another site, in other words a CSRF countermeasure.

The attack script, however, runs inside the victim’s browser, on the same origin as the victim. In that case it can simply do what the legitimate screen does. GET the profile settings page and read the value of the element named token inside it. That alone hands it the token.

A CSRF-protection token does not prevent XSS. Once XSS is in play, the attacker’s code can act as a logged-in legitimate user. What the token protects is against requests sent without authorization from another origin, not against a malicious script running on the same origin.

This distinction pays off when reading security requirements at work, too. “We have CSRF tokens in place, so we are fine” may be correct about CSRF, but it says nothing whatsoever about XSS.

What the File Object Signifies

Line (3) is worth a look as well. It builds a file object whose contents are the document.cookie string, whose file name is a.png, and whose MIME type is image/png.

The contents are just text. It is not a PNG file. The upload succeeded anyway because, as the problem statement says, web app Q did not check the format of the uploaded image file.

Why the Session ID Was Readable, and What HttpOnly Protects

And the reason document.cookie could be read at all is that the cookie did not carry the HttpOnly attribute. RFC 6265 specifies that the HttpOnly attribute “limits the scope of the cookie to HTTP requests” and, in particular, “instructs the user agent to omit the cookie when providing access to cookies via ‘non-HTTP’ APIs (such as a web browser API that exposes cookies to scripts)”7. Had this attribute been set, the session ID alone would have dropped out of the string line (3) grabs, leaving nothing of value to carry out.

What to watch out for here is that HttpOnly hides only the cookies the attribute is set on. If the same origin has cookies without HttpOnly, such as display preferences or analytics IDs, document.cookie keeps returning those. Setting HttpOnly does not make document.cookie empty. What you want to protect is the session ID, so check individually that the session-ID cookie definitely carries it.

7. Exfiltration That Never Leaves the Site — Questions 3(2) and 3(3)

Question 3(2) asks, in 50 characters or fewer, how the attacker can retrieve the uploaded information.

Model answer:Download the member’s avatar image and extract the session ID string from it.3

The Basis for Retrieval Lies in Where the Upload Is Displayed

Recall what the specification in the problem statement said.

The uploaded avatar image is displayed on the member profile settings page and on review pages.

In other words, the “avatar image” that has the victim’s session ID written into it is placed somewhere on the site where it can be viewed. The attacker does not need to do anything special. They open a page on which the victim’s avatar image is displayed and get the URL of that image. The contents are text, so opening it makes the session ID readable.

To add to that, retrieval is this straightforward when the victim’s avatar image appears on a page the attacker can see, such as the review page of a product the attacker has also reviewed. An avatar’s URL is fixed per member, so once it has been seen anywhere, the attacker can go straight to that URL from then on. Either way, all the attacker does is GET a page or an image as a legitimate visitor, which from the site’s point of view is not abnormal traffic.

Follow the Whole Flow From Posting to Retrieval

The victim’s browser does the uploading; the attacker does the retrieving. Follow the diagram with the two actors’ actions kept apart.

Victim's browserWeb app Q(example.jp)AttackerVictim's browserWeb app Q(example.jp)AttackerPreparation stageStores the posted stringas-is(storing it is correct)The victim opens page V★The vulnerability is here★Embeds the stored titleinto HTML without escapingRuns as a script elementReads document.cookie andmakes it the contents of a.pngStores it without checking the formatand publishes it as an avatar imageRetrievalPost the review in 15 parts(comment markers skip the HTML)1GET page V2HTML containing the attack script3GET /user/profile4HTML containing the token5POST /user/upload(uploadfile=a.png, token=…)6GET the victim's avatar image7A file with the session ID written in it8

Egress Controls Do Not Stop It

Drawn out, what makes this route awkward is obvious. From start to finish, the victim’s browser communicates only with the legitimate site (example.jp).

The following table compares countermeasures that watch for suspicious external destinations with countermeasures that stop script execution. Of the settings listed here, only the last one works. CSP is not a condition of the problem statement; read it as supplementary material for practice.

Countermeasure Against this attack Why
Proxy URL filtering Does not work The only destination accessed is an e-commerce site that is legitimate for business purposes
Firewall monitoring of outbound traffic Does not work No traffic to an unfamiliar domain occurs
A Content Security Policy connect-src restriction (a setting such as 'self' that allows the same origin) Does not work Both requests are addressed to the same origin and fall within what the policy allows
A Content Security Policy script-src restriction (a configuration that does not allow 'unsafe-inline') Works The embedded inline script never runs in the first place

Exfiltration via XSS tends to bring to mind a picture of cookies being sent to the attacker’s server, but this question shows plainly that the destination can be the victim’s own site. Wherever the site holds a place that is both writable and readable, that place becomes the drop point. File upload features, public profile fields and public notes fields are typical examples.

With CSP, Keep “Where It Can Talk To” Separate From “What May Run”

A note on CSP: among the directives commonly used in practice, the one that reliably stops this attack is script-src, not connect-src, which restricts destinations. With a configuration that specifies script-src 'self' and does not allow 'unsafe-inline', the inline <script> embedded in the review never runs at all. Understand CSP solely as a way of narrowing outbound traffic and you will misjudge this difference.8

connect-src is not powerless in principle, either. A setting such as connect-src 'none' that forbids even same-origin requests, or an allowlist narrowed to specific endpoints, would stop these two XHRs as well. Few sites can narrow things that far, though, so as a realistic defense script-src is the leading candidate. The accurate understanding is not “same-origin traffic is outside CSP’s scope” but “a common setting that allows the same origin lets it through.”

Question 3(3): What Can Be Done With the Session ID

Question 3(3) asks, in 40 characters or fewer, what can be done with the information obtained.

Model answer:Impersonate a member who accessed page V and use the features of web app Q.3

According to the grading commentary, the percentage of correct answers for this sub-question was high. It states that the impact of an attacker obtaining cookies on an e-commerce site was well understood.4

And rereading the specification in the problem statement, the member profile feature explicitly includes a page for registering credit card information, available only to logged-in members. What lies beyond the impersonation was laid out in the problem statement from the start.

8. Why It Does Not Work From the Attacker’s Own Site — Question 4

Question 4 is the most fundamental question in this whole question set.

Suppose HTML containing the same script as Figure 4 were prepared on a site under a domain the attacker set up, and a logged-in member of web app Q visited that site. The attack would still not succeed, because of a mechanism in the web browser. Describe that mechanism in 40 characters or fewer.

Model answer:The mechanism by which cookies are not sent from a script to a URL on a different domain.3

This section first checks what happens if the script stays exactly as the problem statement has it, and then sorts out the conditions around CORS and SameSite. It is important not to assert, as the basis for the answer, settings the problem statement never states.

Suppose the same script is placed on evil.example and a Company Q member is lured into loading it. What happens?

First, the initial request comes up empty. When a script on evil.example sends an XMLHttpRequest to https://example.jp/user/profile, that is a cross-origin request.

The decisive part is that no cookie is attached. The script in Figure 4 does not set withCredentials, so no cookie is attached to a cross-origin request. From Company Q’s server’s point of view this is an unauthenticated access, and the profile settings page containing the token does not come back. The script cannot obtain the token, and it stops there.9

A Conditional Note: CORS Decides Whether the Response Can Be Read

There is a second wall as well: the response cannot be read. Unless Company Q allows evil.example in Access-Control-Allow-Origin, the browser fails the CORS check, treats this request as a network error, and onload never fires, so the script cannot reach xhr.response. An ordinary site does not return this header to an unfamiliar origin, so in practice it should stop here too.

How Company Q had configured CORS, however, is not written in the problem statement. Even if it did permit access from evil.example, what could be read would be the unauthenticated response, which contains no token, so the attack still would not work. The first point alone, that no cookie is attached, already stops the attack.

That no cookie is attached is a reason you can read off this script. Whether the CORS restriction also applies depends on Company Q’s settings. Do not treat both of these as confirmed facts.

On top of that, the cookie itself cannot be read. What document.cookie returns is the cookies of the origin the script is running on, evil.example. The session ID issued for example.jp is not among them.

IPA’s model answer takes the former, that cookies are not sent to a URL on a different domain, as the answer.

Do not commit these two to memory as the same mechanism. They are separate mechanisms.

  • document.cookie does not return example.jp’s cookies because cookies are managed separately per domain. Between separate sites, that is, separate registrable domains such as evil.example and example.jp, this separation cannot be loosened by any setting. Subdomains under the same domain are a different matter: with the cookie’s Domain attribute, a cookie can be shared between sub.example.jp and example.jp7
  • No cookie is attached to a cross-origin XHR because withCredentials defaults to false. This one does change once the conditions line up. If the script sets withCredentials = true, and the cookie’s SameSite attribute permits cross-site sending, and the server returns an Access-Control-Allow-Origin that names the requesting origin explicitly together with Access-Control-Allow-Credentials: true, then the cookie is sent and the response can be read

A Practical Note: What It Takes Once the Settings Are Changed

Here, keep the reason it actually failed separate from what would be required if the script were rewritten.

There is exactly one thing that can be said with certainty from the problem statement and Figure 4. withCredentials is not set, so no cookie is attached, the access is treated as unauthenticated, and no token is obtained. That alone stops the attack.

The absence of a CORS allowance is another wall that keeps the response from being read, but Company Q’s settings are not written in the problem statement, so treat that as a conditional reinforcement along the lines of “that is how an ordinary site would be configured.”

So what happens if the attacker rewrites it to withCredentials = true? Only at that point does the other condition come into play. Unless the cookie’s SameSite permits cross-site sending, raising withCredentials still leaves that cookie unattached. Today’s browsers treat a cookie with no SameSite as Lax, so by default it falls on the not-attached side.

How SameSite was set on Company Q’s session-ID cookie is not written in the problem statement either. You therefore cannot say the site “was protected thanks to SameSite.” The point is only that SameSite can work against an attacker who comes at you with credentials attached.

In summary, reading an authenticated response from a different domain requires all three of the following to line up: withCredentials = true, a cookie SameSite that permits cross-site sending, and a server that allows the requesting origin under CORS with credentials. A different domain is not automatically safe, but neither is it something a single one of these conditions is enough to break. When auditing your own site, do not take comfort in cookie domain separation; you need to actually check the CORS settings, in particular which origins are allowed for requests with credentials, and the cookie’s SameSite attribute.

That Is Why Stored XSS Is So Valuable

What this sub-question teaches is why an attacker insists on running their own code inside the victim’s site.

Even with a pixel-perfect fake site set up on the attacker’s own domain, the browser treats it as a different site. The other site’s cookies cannot be read through document.cookie, and with an XHR sent without credentials, as in this sub-question’s script, the other site does not even see the request as logged in. The value to the attacker lies in the code running on the victim site’s origin itself. Stored XSS is precisely a means of achieving that.

Do Not Confuse “Can Send” With “Can Read”

Do not read this as “no authenticated request whatsoever can be sent from a different domain,” though. If the cookie’s SameSite is set to permit cross-site sending, SameSite=None for example, a malicious page can indeed deliver a request with cookies attached to the other server. A form POST is the typical case: what CORS mainly controls is whether a script can read the response, not whether the request reaches the server (although a preflight goes first when a custom header is added or the body is sent as application/json, so if it is not permitted, the actual request is never sent). This is why CSRF works as an attack, and why a CSRF-protection token is needed.

In other words, what a script on another origin cannot do by default is read the other party’s cookies and read the response, not send a request.

What is more, these two “cannot read” cases differ in strength. The domain separation behind document.cookie cannot be loosened by any server-side setting, whereas whether the response can be read depends on the other server’s CORS configuration. If the server permits the requesting origin, returning a concrete origin in Access-Control-Allow-Origin and, for credentialed requests, Access-Control-Allow-Credentials: true alongside it, a script on another origin can read the response. That is exactly why you should check your own site’s CORS settings.

What makes XSS special is that it vaults over all these conditions and acts as the victim site’s origin from the outset.

Put differently, XSS is not “a defect where odd characters show up on screen”; it is a defect that puts the attacker in the same position as a legitimate user of your own site. That difference in understanding is what separates how the priority gets judged.

9. Countermeasures That Worked and Countermeasures That Did Not

Here is everything so far in a single table. It is the part of this article we most want you to take away. Read it in terms of “which stage of this attack does it stop,” not “is there a countermeasure.”

First, Compare Company Q’s Countermeasures Against This Attack

Mechanism Company Q had or lacked Against this attack Why
Communication over HTTPS Did not work It protects the channel, and has nothing to do with a script embedded in the page
Input character limits of 50 characters for the review title and 300 for the review body Did not work The payload was posted split across 15 posts, and the HTML in between was skipped over as a JavaScript comment
The upload token (a CSRF countermeasure) Did not work A script on the same origin can obtain the token through the legitimate procedure
An upload feature that requires login Did not work What is doing the work is the logged-in victim’s own browser
Escaping at output time (absent) Works (fundamental fix) The posted string is no longer interpreted as HTML, so the script never runs at all
The HttpOnly attribute on the cookie (absent) Works (fallback countermeasure) The session ID can no longer be read from document.cookie
Format checking and re-encoding of uploaded files (absent) Works (fallback countermeasure) Raw text cannot be stored as a PNG, and a string hidden in the metadata is wiped out by re-encoding. Data encoded as the image’s pixels, though, survives

The top four did nothing whatsoever against this attack. Mapping the bottom three onto the categories in IPA’s “How to Secure Your Website,” the first is a fundamental fix and the remaining two are fallback countermeasures5.

One caveat: this does not mean the top four are things IPA does not recognize as countermeasures. IPA’s categories are fundamental fixes and fallback countermeasures, and checking the content of input values is in fact listed as a fallback countermeasure. It comes with the proviso that “this countermeasure is effective only in limited cases,” however, and this case fell squarely inside that limitation.

They Stop Different Stages, Which Is What Makes It Defense in Depth

And the important point is that any one of the bottom three would have broken the attack chain that was carried out in this question.

  • With escaping in place, the script never runs at all
  • With HttpOnly set, the script runs but cannot read the cookie
  • With format checking in place, the cookie can be read but raw text cannot be uploaded as a PNG

This is exactly the situation in which defense in depth means something. Fallback countermeasures are still no substitute for the fundamental fix, though. Even with HttpOnly set, if the XSS remains, the attacker can carry out arbitrary operations in the victim’s browser, such as buying products or changing registered information. Attacks that do not even need to steal the session ID can be designed without limit.

Why the Table Says “Re-encoding” and Not Just “Format Checking”

There is a reason the last row of the table is written as a two-stage “format checking and re-encoding.”

Distinguish This Case’s Raw Text From a Correctly Formatted Image

Company Q in the problem statement did not check the format of image files at all, so simply adding a format check stops this attack. That, however, holds only for the case where the attacker sends raw text. An attacker can also assemble a valid PNG file and slip the string into its metadata area or somewhere similar. In that case a check of “is the format correct” passes.

So if you want to narrow this route, you have to go as far as re-encoding uploaded images on the server and serving those, that is, not storing the original byte sequence as-is.

Re-encoding Is Not a Cure-All Either

Re-encoding does not close the route completely, either. If the attacker draws the session ID in as the image’s own pixels and builds a valid PNG, ordinary re-encoding preserves that visible information, so the attacker can download it and read it off. What re-encoding stops is the variant that sends a raw byte sequence and the variant that hides data in the metadata area, not information encoded as the content of the image.

In other words, as long as the site holds a place that is both writable and readable, that place cannot be completely closed off as an exfiltration route. This captures the nature of fallback countermeasures well. It is not enough for a fallback countermeasure to stop the attack observed right now; it has to be designed with an eye on whether it still holds when the attacker changes technique slightly. And no matter how many you stack up, they are no substitute for escaping at output time, which is the fundamental fix.

Serving From a Separate Domain Is a Countermeasure Against a Different Threat

A design in which uploaded files are served from a domain separate from the main site is also often recommended, but it is not a countermeasure against this exfiltration route. The attacker can download the image from a separate domain just as easily, and the same-origin policy is not a mechanism for keeping published byte sequences secret. What separate-domain serving does work against is the threat of the uploaded file itself executing as active content on the victim site’s origin, the kind where HTML or SVG is uploaded. Understand it separately, as a countermeasure against a different threat.

How You “Judge by the Contents” Differs by File Format

The idea of judging a file by its contents rather than by the sender’s declaration (the extension or the Content-Type) is a basic practice needed well beyond the web. What “judging by the contents” amounts to, though, differs by format. For a format like PNG, which has a fixed byte sequence at the start, you can check that. CSV, by contrast, has no standardized signature, and a BOM at the start indicates only the character encoding and is no proof that the contents are CSV. As covered in CSV Is Not “Just Text”: A Practical Guide to CSV Handling in C# Business Apps (Encoding, Excel Compatibility, Injection Defense), for CSV it comes down to checking whether it parses under the expected dialect and whether it stays within the expected schema and record-count limit.

10. Angles for Checking Your Own Code

Boiled down into a code-review checklist, this question set comes out as follows. For any web application with member accounts and posting features, it is usable as-is.

Items 1 to 3 cover input and output, item 4 the cookie, items 5 and 6 the upload, and items 7 and 8 CSP and the token. The first half inspects the implementation that removes the root cause; the second half inspects the range over which fallback countermeasures work.

  1. Is escaping at output time in effect at every output point? Enumerate every place where the template engine’s automatic escaping is explicitly turned off (raw, | safe, dangerouslySetInnerHTML and the like), and check that you can explain, for each one, why turning it off is acceptable
  2. Are you counting input-side restrictions as XSS countermeasures? Character limits and restrictions on which character types may be entered are requirements of the specification, not the fundamental fix for XSS
  3. Are you escaping on input? The role of the input side is validation that rejects values outside the specification, not escaping. At input time the destination (HTML, CSV, JSON, email, logs) is not yet decided, so escaping on input invites double escaping and data corruption. Assembling SQL statements is the same: the fundamental fix there is placeholders
  4. Do your cookies carry HttpOnly, Secure and SameSite? Check the session-ID cookie in particular
  5. Do you check the format of an uploaded file by its contents rather than by the extension or the Content-Type? Information the sender declares is no material for validation. A format check alone, though, lets through the technique of slipping data into the metadata area of a valid image
  6. Do you re-encode uploaded images before serving them? Are you returning the original byte sequence as-is? Understand, though, that even re-encoding leaves information encoded as pixels intact, so a place that is both writable and readable cannot be closed off completely. Note also that serving from a separate domain is a countermeasure against the threat of an uploaded file executing on your own site’s origin, not against this exfiltration route
  7. Is your configuration able to stop inline scripts through CSP? The criterion is not “is 'unsafe-inline' written there” but “are inline scripts actually permitted.” When script-src carries a nonce or a hash, browsers from CSP Level 2 onward ignore 'unsafe-inline', so under a backward-compatible policy that lists both 'unsafe-inline' and a nonce, an injected script without the nonce is blocked8
  8. Can you explain what the token protects? A CSRF-protection token does not prevent XSS. Each needs its own separate countermeasure

The first item in particular is a typical pattern turned up by real investigations. It is not unusual to find that a team believed it was safe because the framework escapes automatically, yet automatic escaping had been turned off in exactly one place to satisfy an individual requirement to “insert HTML as-is.”

In Closing — The Real Ability This Question Tests

The list of countermeasures covered so far is summarized in “The Bottom Line First” at the top. To close, one more word about the question itself.

What makes it well constructed as an exam question is that it asks not “do you know what XSS is” but “can you tell which of the countermeasures already at hand are actually working.”

It is not that Company Q had done nothing. It communicated over HTTPS, set character limits on input, required a token for uploads, and restricted the features to logged-in members. Listed out, it looks like a reasonably well-defended build. Everything was slipped past anyway. Conversely, the three things it lacked are all unglamorous, none of them the kind of feature that goes into a release note.

Security discussions are difficult because the number of countermeasures is not proportional to the strength of the defense. The question is whether you can state, one by one, not “what are we doing” but “which stage of which attack does this countermeasure stop.” That is not an ability needed only for a certification exam; it is the judgment most needed both by the people receiving an explanation of security measures and by the people implementing them.

KomuraSoft LLC handles the building of websites with member accounts and posting features, and design reviews that check whether an existing web application has the same hole.

References

  1. IPA (Information-technology Promotion Agency, Japan), Question Booklets, Score Allocations, Model Answers and Grading Commentary (FY2023, Reiwa 5), containing “Autumn 2023 (Reiwa 5) Registered Information Security Specialist Examination, Afternoon, Questions.” On the features of Company Q’s web app Q (member registration, login and the issuing of a session ID as a cookie, the input character limits of 50 characters for the review title and 300 characters for the review body in the product review feature, avatar image upload and credit card information registration in the member profile feature, the avatar image upload taking an image file and a token as parameters, and the uploaded avatar image being displayed on the member profile settings page and on review pages), the phenomenon in which only 2 of the 16 reviews were displayed on page V, the fact that the HTML of page V contained 15 posts by member A and a long script, the contents of the extracted script, and the facts that web app Q had a vulnerability by which a script entered by a member would be executed, that the HttpOnly attribute was not set on the cookie, and that the format of uploaded image files was not checked. The text of Questions 1 to 4 also comes from this booklet.  2

  2. IPA (Information-technology Promotion Agency, Japan), Frequently Asked Questions About the Examinations. On the facts that, for use of the past examination questions IPA publishes, no permission or usage fee is required except where the law specifically provides otherwise; that IPA has nonetheless not waived copyright; that the source must be stated in the form “fiscal year, session, examination category, time slot, question number, and so on” (the example given is “Source: Spring 2019 (Heisei 31) Fundamental Information Technology Engineer Examination, Morning, Question 1”); and that any partial modification of a question must be noted as well. 

  3. IPA (Information-technology Promotion Agency, Japan), Autumn 2023 (Reiwa 5) Registered Information Security Specialist Examination, Model Answers. On the aim of Question 1 (taking incident response to the exploitation of a vulnerability in a web application program as its subject, it tests the ability to read the exploited vulnerability and the problems behind it out of HTML and ECMAScript and to devise countermeasures), and on the model answer for each sub-question (Question 1(1): “(b) stored XSS”; Question 1(2): “Apply escaping to the review title before output.”; Question 2: “The attacker made the posts in several parts, such that the HTML would be commented out and become a single script.”; Question 3(1): “It uploads the session ID as an avatar image, together with the token obtained from the XHR response.”; Question 3(2): “Download the member’s avatar image and extract the session ID string from it.”; Question 3(3): “Impersonate a member who accessed page V and use the features of web app Q.”; Question 4: “The mechanism by which cookies are not sent from a script to a URL on a different domain”).  2 3 4 5 6 7 8 9

  4. IPA (Information-technology Promotion Agency, Japan), Autumn 2023 (Reiwa 5) Registered Information Security Specialist Examination, Grading Commentary. On the fact that the overall percentage of correct answers for Question 1 was average; on Question 1(1), that “a number of candidates were seen to answer incorrectly with ‘DOM Based XSS’, apparently because the script used the DOM,” together with the remark that vulnerabilities should be understood accurately down to their characteristics and their countermeasures; on Question 2, that “some answers appeared to reflect insufficient checking, such as ‘the attacker deleted the input restriction with the developer tools and then posted,’” together with the remark that candidates should cultivate the ability to check carefully the traces an attacker leaves and to grasp the method of attack accurately; and on the fact that the percentage of correct answers for Question 3(3) was high, the impact of an attacker obtaining cookies on an e-commerce site being well understood.  2 3 4 5

  5. IPA (Information-technology Promotion Agency, Japan), How to Secure Your Website. On the fact that, for 11 types of website vulnerability, it presents the threats and the countermeasures divided into “fundamental fixes” (implementations that remove the cause of the vulnerability itself) and “fallback countermeasures” (measures that lower the success rate of an attack, or the damage, when a vulnerability remains); that applying escaping to every element output to the web page is listed as the fundamental fix for cross-site scripting; and on the accompanying security implementation checklist and the separate volumes “How to Call SQL Safely” and “Web Health Check Specification.”  2 3

  6. IPA (Information-technology Promotion Agency, Japan), How to Secure Your Website - 1.1 SQL Injection. On the facts that “implement all SQL statement construction with placeholders” is listed as the fundamental fix for SQL injection, with static placeholders (prepared statements) held to be the form in which a vulnerability cannot arise in principle; that, as the implementation for the case where a SQL statement is built by string concatenation, it states “when the SQL statement is assembled by string concatenation, use the database engine’s API that performs escaping and the like to correctly form the literals of the SQL statement,” so that escaping is applied to generating the literals that make up the SQL statement and not at the point where the input is received; and that, alongside these, “do not specify SQL statements directly in the parameters passed to the web application” is listed as a fundamental fix, while “do not display error messages in the browser as-is” and “grant appropriate privileges to the database account” are listed as fallback countermeasures. 

  7. IETF, RFC 6265: HTTP State Management Mechanism, Section 4.1.2.6 “The HttpOnly Attribute”. On the facts that the HttpOnly attribute limits the scope of the cookie to HTTP requests and, in particular, instructs the user agent to omit the cookie when providing access to cookies via “non-HTTP” APIs such as a web browser API that exposes cookies to scripts. Also on the fact that the Secure attribute (Section 4.1.2.5) restricts the cookie to being sent only over a secure channel.  2

  8. W3C, Content Security Policy Level 3. On the control of inline script execution via script-src, the control of destinations via connect-src, and the treatment of unsafe-inline in a source list that contains a nonce or a hash. It is the basis for the practical notes in Sections 7 and 10, and is not a document that shows Company Q’s settings.  2

  9. WHATWG, XMLHttpRequest Standard — The withCredentials getter and setter. On the fact that it controls whether credentials are included in a cross-origin request, and that its initial value is false. Section 8 cites it as the basis for reading the absence of a withCredentials setting in the problem statement’s script as meaning that no cookie is attached. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Website Development

On a website with member accounts and posting features, the points in this article, such as escaping at output time and setting cookie attributes, translate directly into build quality.

Frequently Asked Questions

Common questions about the topic of this article.

Why is the XSS in this question 'stored'? The script manipulates the DOM, so isn't it DOM Based XSS?
The type of vulnerability is decided by where the attacker's string turns into executable code, that is, by whether the vulnerable output point (the sink) is on the server side or the browser side. In this question, the server embedded the review title the attacker posted directly into the HTML and returned it. The sink is in the server's output processing, and the string is stored and then handed to everyone who opens the page from then on, so this is stored XSS. DOM Based XSS refers to the category in which JavaScript in the browser makes a value executable, for example by assigning it to innerHTML. Whether the embedded script uses DOM APIs such as XMLHttpRequest or getElementById has nothing to do with the type of vulnerability. Note, too, that even if the server returns a stored value as harmless text or JSON, it becomes DOM Based XSS the moment JavaScript in the browser passes it to innerHTML. Judge by where the value became executable, not by whether the string is visible in the response. IPA's grading commentary also notes that a number of candidates answered DOM Based XSS incorrectly, apparently because the script used the DOM.
The review title had a 50-character input limit. Why could a long script still run?
The attacker split the payload into lengths that fit into a single post and posted it several times. The first post opens a script tag partway through the title and opens a JavaScript block comment (a slash and an asterisk) at the end of the title. The second post closes that comment at its start, writes the next statement, and opens another comment at its end, and that pattern repeats. Written this way, all the HTML that falls between one post and the next (div tags and so on) ends up inside a JavaScript comment and is ignored, so only the split fragments join together and run as a single script. The input character limit only restricted the length of each individual post; it did not restrict the number of posts.
Where was the stolen session ID sent?
It was not sent to an external server. The script took the contents of the cookie as the contents of a file, named it "a.png", set the MIME type to "image/png", and sent it to the avatar-image upload feature of the very e-commerce site the victim was using. Because the uploaded avatar image is displayed on the review page and elsewhere, the attacker could simply download that image and pull the session ID out of the string inside it. The victim's browser communicated only with the legitimate site, and since no suspicious external traffic occurs, this is a route that proxy URL filtering and firewall monitoring of outbound traffic cannot notice.
The upload was supposed to require a token. Why did the attack succeed anyway?
Because that token is a mechanism for rejecting unauthorized requests from another site (a CSRF countermeasure), not a countermeasure designed with a script running on the same site in mind. The attack script first accesses the profile settings page with XMLHttpRequest, receives the token exactly as the legitimate screen does, and then performs the upload with that token attached. Since XSS makes the attacker's code run on the same origin as the victim, it can do anything a legitimate user can do. A CSRF-protection token does not prevent XSS.
I hear that escaping input matters for SQL injection. Is output time unique to XSS?
No. For SQL injection as well, "escaping on input" is not the countermeasure. What IPA lists as the fundamental fix is to "implement all SQL statement construction with placeholders." Escaping is listed as an alternative for the case where a SQL statement is built by string concatenation, but even there it is for "correctly forming the literals of the SQL statement," that is, it happens at the moment the SQL statement is constructed, not at the point where the input is received. The shared principle is that escaping is performed where it is decided which grammatical world the data is heading out into. If it goes out as HTML, HTML escaping; if it becomes part of a SQL statement, placeholders; if it becomes part of a shell command, the shell's own conventions. The reason you must not escape on input is that the output destination is not decided at that point. The same data goes out to HTML pages, to CSV, to JSON, to notification email and to logs, so applying HTML escaping on input makes entity references appear verbatim in the CSV, or leaves the database holding something different from the original input so that search and aggregation go wrong. This does not mean the input side does nothing. The input side's role is validation, not escaping: rejecting values that are impossible under the specification.
What countermeasures should I take back to my own work from this question?
The fundamental fix is to apply escaping immediately before output to HTML to all user input, including the review title. On top of that, the fallback countermeasures are to set the HttpOnly attribute on the session-ID cookie so scripts cannot read it, to re-encode uploaded images on the server so the original byte sequence is not kept, and to arrange a Content Security Policy configuration that can stop inline script execution. Had Company Q in this question implemented even one of these, the attack chain would have been broken somewhere. Fallback countermeasures can be evaded if the attacker changes technique, however, so they are no substitute for escaping, which is the fundamental fix.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog