Registered Information Security Specialist Exam, Autumn 2023 (Reiwa 5) Afternoon Q1 Commentary — The Stored XSS Where 16 Reviews Show Up as Only 2
· Go Komura · Registered Information Security Specialist, SC Exam, XSS, Cross-Site Scripting, Web Application, Information Security, Vulnerability, IPA, Session Management
“There are supposed to be 16 reviews, but only 2 are showing.”
The Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5), Afternoon Question 1 opens with exactly this complaint from a user1. Nothing looks wrong with the screen flow, and no error appears. It’s just that the displayed count doesn’t match.
The cause was stored cross-site scripting (XSS). What makes this question interesting, though, isn’t the answer “XSS” itself. It’s that every one of the “plausible-looking countermeasures” Company Q (the e-commerce operator in the problem statement) already had in place was slipped straight past. The 50-character limit on the review title was defeated, the token required for the upload was obtained through entirely legitimate steps, and the stolen session ID was exfiltrated without ever being sent to an external server.
This article works through the question set item by item, laying out where the line falls between the countermeasures that didn’t work and the ones that would have.
What you’ll get from this article is, on top of the exam solutions (the model answer for each question and its rationale), a practical, complete picture of XSS countermeasures you can use at work. If you’re reading for exam preparation, read the section for each question; if you only need the code-review angle for real work, the article is written so that Sections 9 and 10 also make sense if you read them first.
For the broader question of what criteria to use when checking the security of an entire website, 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 vulnerability types covered there — XSS — and digs into it through a worked example.
1. The Bottom Line First
- The vulnerability type is stored XSS. The attacker’s string was saved on the server and, from then on, output into the HTML sent to everyone who opened the page. Whether the embedded script uses DOM APIs is a separate question from whether it counts as DOM Based XSS.
- The input character limit did not work as an XSS countermeasure. The attacker split the payload across 15 posts and had the HTML sitting between posts skipped over as a JavaScript comment, stitching the fragments into a single script.
- The upload token (a CSRF countermeasure) also did not work as an XSS countermeasure. The attack script obtained the token through the same steps as the legitimate screen. As long as it runs on the same origin, it can do anything a legitimate user can do.
- The stolen session ID was never sent externally. The script placed the cookie’s contents on the site’s own avatar-upload feature as an image file named “a.png”, and the attacker simply viewed it, like anyone else, to retrieve it. Egress controls cannot detect this.
- The problem statement explicitly identifies three things Company Q lacked: output-time escaping (the fundamental fix), and the HttpOnly attribute on the cookie and checking the format of uploaded files (both fallback countermeasures). Any one of them would have broken the attack chain somewhere. Format checking, though, needs to go as far as re-encoding, because a changed attack technique can slip past it.
- Although it doesn’t appear in the problem statement, CSP’s
script-srccan break the same chain too. With a configuration that doesn’t allow'unsafe-inline', the embedded inline script would never execute in the first place.
2. About the Source Material — Attribution and How This Article Handles It
The question examined here is the following.
Source: Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5), Afternoon, Question 1
IPA states that, for the past exam questions it publishes, no permission or usage fee is required except where specifically provided for by law. This does not mean IPA has waived copyright, however: it requires attribution in the form “fiscal year, session, examination category, time slot, question number, etc.”, and requires that any modification made to a question also be noted2.
This article does not reproduce the HTML or scripts printed in the exam booklet as-is. Where necessary to explain how the mechanism works, we replace them with equivalent sample code we wrote ourselves. The question text and model answers are also treated in summarized form. The original exam booklet, model answers, and grading commentary can be downloaded free of charge from IPA’s site, so we recommend keeping them open alongside this article as you read1 3 4.
Correspondence Between the Questions and This Article’s Sections
For readers following along with the exam booklet open, here is how each question maps to a section of this article. Feel free to start reading from whichever question you want to work through.
| Question | What it asks (character limit) | Corresponding section |
|---|---|---|
| Question 1(1) | Which type of XSS vulnerability was exploited (multiple choice, 3 options) | 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–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 makes the attack fail from the attacker’s own domain (40 characters or fewer) | Section 8 |
If you don’t need the exam questions and only want the practical takeaways, start with Section 9 (the list of countermeasures that worked and those that didn’t) and Section 10 (code-review checkpoints). If you want to see the whole attack chain in a single view, the sequence diagram in Section 7 gives the full picture from posting to retrieval.
Correspondence Between the Exam Booklet’s Text and This Article’s Samples
So you can cross-check against the original text, here is a summary of what was replaced and how.
| What the exam booklet says | How this article treats it | Where it appears |
|---|---|---|
| The HTML of Page V (including the split-posted review titles) | Not reproduced as-is; we wrote an equivalent sample that condenses the same mechanism into 3 posts | Section 5 |
| The extracted attack script (about 20 lines) | Not reproduced as-is; we wrote equivalent JavaScript that performs the same processing. Comments in the code are for this article’s own explanation | Section 6 |
| The text of each question | Summarized while preserving its gist (conditions such as the character limit use the original values) | The opening of each of Sections 4–8 |
| Model answers | The model answers published by IPA3 | Throughout Sections 4–8 |
| Grading commentary | The relevant portions of the grading commentary published by IPA4 | Sections 4, 5, and 7 |
| The problem statement’s specification (Company Q, Page V, Members A/B, features and character limits, etc.) | Summarized in line with the original text | Section 2, “The Setting of the Problem” |
The Setting of the Problem
The scenario involves Company Q, a 100-employee clothing e-commerce business. It runs its e-commerce site on an in-house-developed system called “Web App Q”, and users access it over HTTPS. The setup for this problem is that Company Q has just added a product review feature for members.
There are five specification points worth keeping 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 a 50-character limit and the review body has a 300-character limit, both free-text fields |
| 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 an image file and a token as parameters to /user/upload. Succeeds only when the token matches the one issued at /user/profile |
| Avatar image display | An uploaded avatar image is displayed on the member profile settings page and on review pages |
The last two rows will matter later.
3. The Symptom — Why 16 Became 2
A member reports: “The review page for a plain T-shirt (Page V) is supposed to show 16 reviews, but only 2 are displayed.” When N, from the development department, opens Page V, this is what the screen shows:
- The header reads “16 reviews”
- One review from Member A (title “Good”, body “Nice shirt!”)
- One review from Member B
- The footer reads “That’s all 16 reviews”
The displayed count says 16, but only 2 are actually listed. Checking the HTML at this point reveals 15 posts from Member A, with a long script embedded inside them.
In other words, all 16 reviews’ worth of content is actually output into the HTML. The reason only 2 are visible is that most of it ends up inside a <script> element, so the browser stops treating it as content to display.
To be precise, the range that gets swallowed is not “all 15 posts”. The opening <script> tag appears partway through the first review’s title (right after “Good”), and the closing </script> tag appears at the end of the 15th review’s title. As a result:
- The first card (avatar, display name, date, star rating, and the title up to “Good”) sits before
<script>, so it is displayed - Everything from there up to the end of the 15th title is swallowed as the contents of the script element
- What remains of the 15th post (the body text “Nice shirt!”) sits after
</script>, so it is displayed
As a result, the header portion of the first post joins up with the body of the 15th, so on screen it looks like “a single review from Member A with the title ‘Good’ and body ‘Nice shirt!’”. Add Member B’s one review, and you get 2. The attacker’s choice to put the natural-looking string “Good” at the start of the first post and “Nice shirt!” in the body of the 15th was presumably to keep the broken rendering from looking unnatural.
This way of reading the symptom — “the count is right but the display isn’t” — is useful in real-world work too. It’s an entry point for suspecting that the structure of the output HTML has been broken, rather than a bug in the display-count logic.
4. Why “Stored” XSS — Question 1
Question 1 asks you to choose the type of XSS vulnerability used in this attack from three options: DOM Based XSS, Stored XSS, or Reflected XSS. The correct answer is Stored XSS.
And IPA’s grading commentary on this question says the following.
The correct-answer rate was about 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 certainly touches the DOM. But that has nothing to do with the vulnerability’s type.
The Deciding Factor: Where the Attack String Turns Into a Script
What separates the three types is where the attack string turns into executable code — in other words, 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 process | The request itself — for example, a URL parameter the attacker prepared |
| Stored XSS | The server’s HTML-assembly process | Data saved on the server side |
| DOM Based XSS | JavaScript running in the browser (assignment to innerHTML, eval, document.write, etc.) |
A URL fragment, postMessage, a value received from the server, etc. |
In this problem, the server embedded the attacker’s posted review title directly into the HTML it returned. The sink is in the server-side output process, and the string is stored in the database and then distributed to everyone who subsequently opens Page V. Hence it is stored XSS.
flowchart TD
A["The attacker's string<br/>was executed as a script"] --> B{"Where did it become<br/>executable?"}
B -->|"When browser-side JS<br/>passed it to<br/>innerHTML, etc."| C["DOM Based XSS"]
B -->|"It was inside HTML<br/>the server<br/>assembled"| D{"Is that string<br/>saved on<br/>the server?"}
D -->|"Saved -<br/>output to everyone from then on"| E["Stored XSS"]
D -->|"Not saved -<br/>only for that request"| F["Reflected XSS"]
It’s risky to oversimplify this as “if the attack string appears in the server’s response, it’s stored or reflected XSS.” Even if the server returns the value as harmless text or JSON, it becomes DOM Based XSS the moment browser-side JavaScript assigns it to innerHTML (this is the so-called stored DOM XSS, where a saved value is the root cause). In this case, what needs fixing is not the server’s output processing but the client-side sink, so judge by where the string became executable, not by whether it’s visible in the response.
What the embedded script does (touch the DOM, communicate over the network, read cookies) and how that script got into the page need to be thought about separately. The reason it’s easy to confuse the two is not that it makes it hard to choose a countermeasure — it’s that it makes you choose the wrong one. Judging it as DOM Based XSS leads to the conclusion “just fix the client-side JavaScript”, but 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. The model answer is “Apply escaping to the review title before it is output.”
What matters here is that the ordering “before it is output” is stated explicitly. You don’t escape when input is received; you escape immediately before outputting as HTML, according to the context of the destination. IPA’s “How to Secure Your Website” also lists, as the top fundamental fix for XSS, “apply escaping to every element output to the web page”5.
“Isn’t Escaping on Input Important for SQL Injection?”
This question always comes up at this point. The short answer: for SQL injection too, “escaping on input” is not the countermeasure.
What IPA lists as the fundamental fix for SQL injection is “implement all SQL statement construction using placeholders.” Escaping is mentioned as an alternative for cases where you have no choice but to build SQL statements by string concatenation, but even there the wording is: “when SQL statement construction is done by string concatenation, use the database engine’s API that performs escaping etc. to correctly form the literals of the SQL statement” — meaning escaping happens at the moment the SQL statement is constructed6, not at the point input is received.
In other words, this has exactly the same structure as the HTML case. The shared principle is this:
Escaping is done at the point where it’s decided which grammar the data is heading out into.
- If it’s going out as HTML, use HTML escaping
- If it’s becoming part of a SQL statement, use placeholders (or, if unavoidable, escape it as a SQL literal)
- If it’s becoming part of a shell command, handle it the shell’s way
Why not at input time? Because at the point input is received, where that data will eventually go is not yet decided. The same review body might go out into an HTML page, a CSV export, a JSON API response, a notification email, or a log. If you apply HTML escaping at input time, the string & shows up verbatim in the CSV, the contents of the database end up different from the original input, and both search and aggregation go wrong. It’s also fertile ground for double escaping.
So Should the Input Side Do Nothing at All?
No. But what the input side should do is validation, not escaping. The two have different roles.
- “Reject” on input — refuse values that are impossible under the specification. A postal-code field rejects anything other than 7 digits; a quantity field rejects negative numbers. This is an independently necessary process for protecting the correctness of the data
- “Escape” on output — safely express the accepted data in the grammar of wherever it’s going out to. This is the fundamental fix for the vulnerability
It’s also important not to expect too much of input-side validation as an XSS countermeasure. IPA touches on the method of checking whether input values conform to the application’s specification, and then states explicitly that this countermeasure’s effectiveness is limited, and it does not work as a countermeasure when the application’s required specification allows a wide range of character types as input — so relying on this method is not recommended5.
The review title and review body in this problem were exactly that: free-text fields specified to allow a wide range of character types. There are upper limits — 50 and 300 characters — but the number of characters needed to run a script is far fewer than that, so having an upper limit at all provides no defence. We’ll look at what actually happened at Company Q in the discussion of Question 2, next.
5. How the 50-Character Limit Was Defeated — Question 2
Question 2 is the highlight of this problem.
Regarding Figure 3, describe in 50 characters or fewer the method used to get a script longer than the input character limit to execute.
The model answer is: “Posted multiple times in a way that commented out the HTML so it became a single script.”
The attacker split the payload into lengths that fit within a single post and posted 15 times. The key issue is how to deal with the HTML (</div>, <div class="...">, and so on) that inevitably sits between each post — as JavaScript, this HTML would be a syntax error.
The trick used here is JavaScript’s block comment. What follows is a simplified version we wrote ourselves to explain the mechanism (it is not a direct quotation of the exam booklet’s figure).
Suppose the following is posted into the review title field across 3 separate posts.
Post 1: Wonderful<script>a=1;/*
Post 2: */b=2;/*
Post 3: */c=3;</script>
The server outputs each of these as a review title into 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>
From the browser’s perspective, everything from the first <script> to the last </script> is a single script element. Its contents look like this:
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, so all that actually executes is a=1; b=2; c=3;. And because the part skipped over as a comment is still, after all, the content of a script element, it is not displayed on screen. This is the true nature of the “16 looks like 2” symptom.
What to Take Away From This
Don’t get the cause and effect backwards here. The input character limit fails as an XSS countermeasure not because the number of posts was unlimited. It’s because a script can already run with just 50 characters. Adding a single event-handler attribute, or placing a single tag that loads an external script, both fit within a few dozen characters. Even if posting had been limited to a single post, the character limit still would not have provided a defence.
So what was the split posting actually for, then? It was a means of bringing in a roughly 20-line script in its entirety. What the attacker wanted to do was long, so they made up for it with the number of posts — that’s a separate matter from the reason the character limit itself was defeated. You need to think about the description of the technique and its evaluation as a countermeasure separately.
The same is true of “input-side restrictions” in general.
- Character limits, restrictions on allowed character types, and front-end validation — these are needed for specification reasons, but they are not a substitute for the fundamental fix for XSS
- An attacker always has room to get around input-side restrictions using means such as splitting, encoding, or submitting through a different route
- The place that needs protecting is the moment data goes out as HTML
IPA’s grading commentary also notes, on this question, that “some answers were seen that appeared to reflect insufficient checking, such as ‘removed the input restriction using developer tools before posting.’” It’s true that front-end restrictions can be removed with developer tools. But what this question required was reading, from the traces left in the HTML, what actually happened. The HTML in Figure 3 shows 15 posts, each within the length limit, lined up in a row, each with comment markers at its start and end. This is not a trace of the limit being “removed” — it’s a trace of the limit being “worked around.” It should be read as testing the discipline of checking, one by one, what the attacker actually left behind.
The design mindset of how far and how to validate input values is also laid out, as a design that distrusts 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 N extracted is about 20 lines long. Question 3(1) asks for what lines 6 through 20 of it — the part that runs after the first communication succeeds — do, in 60 characters or fewer.
The model answer is: “Uploads the session ID as an avatar image, together with the token obtained from the XHR response.”
Below is equivalent code we wrote ourselves to explain this behaviour (it is not a direct quotation of the exam booklet’s figure).
// ① 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, not as text
xhr.send();
xhr.onload = function () {
// ② Read the upload token using the same steps as the legitimate screen
const token = xhr.response.getElementById("token").value;
// ③ Turn the cookie (which includes the session ID) directly into the contents of a PNG file
const file = new File([document.cookie], "a.png", { type: "image/png" });
// ④ Send it to this 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 Solely to Get Past Company Q’s CSRF Defence
What stands out about this script is that the entire first half is spent solely on obtaining the token.
When uploading an avatar image, Company Q required that the value “match the token issued at /user/profile.” This is a mechanism to prevent a fake form placed on another site from triggering an upload without permission — in other words, a CSRF countermeasure.
But the attack script runs inside the victim’s browser, on the same origin as the victim. So it can simply do exactly what the legitimate screen does: GET the profile settings page and read the value of the token element inside it. That alone is enough to obtain the token.
A CSRF-protection token does not prevent XSS. Once XSS succeeds, the attacker’s code can behave as “a logged-in, legitimate user.” What the token protects against is “a request sent without permission from a different origin,” not “a malicious script running on the same origin.”
This distinction matters when reading security requirements in real work, too. The explanation “we’re fine because we have a CSRF token” may be correct about CSRF, but it says nothing about XSS.
What the File Object Means
Line ③ is worth a close look too. It creates a file object whose contents are the string from document.cookie, whose file name is a.png, and whose MIME type is image/png.
The contents are just plain text. It is not a PNG file. Even so, the upload succeeds because, as the problem statement says, Web App Q did not check the format of the uploaded image file.
And document.cookie was readable at all because the cookie had no HttpOnly attribute set. RFC 6265 defines the HttpOnly attribute as: “limits the scope of the cookie to HTTP requests. In particular, the attribute 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. If this attribute had been set, the session ID alone would have dropped out of the string that line ③ grabs, making it worthless to exfiltrate.
Worth noting here is that HttpOnly hides only the cookies that carry that attribute. If the same origin has cookies without HttpOnly (a display-preference cookie, a tracking ID, and so on), document.cookie keeps returning those. Setting HttpOnly does not mean document.cookie becomes empty. Since what you want to protect is the session ID, check individually whether the session ID’s cookie definitely has it set.
7. Exfiltration That Never Leaves the Site — Question 3(2) and (3)
Question 3(2) asks, in 50 characters or fewer, “how can the attacker obtain the uploaded information?” The model answer is: “Downloads the member’s avatar image and extracts the session ID string from it.”
Recall what the problem statement’s specification said:
An uploaded avatar image is displayed on the member profile settings page and on review pages.
In other words, the “avatar image” carrying the victim’s session ID ends up in a place that can be viewed on the site. The attacker doesn’t need to do anything special. They just open a page where the victim’s avatar image is displayed and grab that image’s URL. Since its contents are text, opening it reveals the session ID.
To add a bit more: this retrieval works straightforwardly when the victim’s avatar image appears on a page the attacker can see (such as the review page for a product the attacker has also reviewed). Because the avatar’s URL is fixed per member, once it’s been seen anywhere the attacker can go straight to that URL afterward. Either way, all the attacker does is GET a page or image as an ordinary viewer, which looks like nothing abnormal from the site’s point of view.
sequenceDiagram
autonumber
participant AT as Attacker
participant Q as Web App Q<br/>(example.jp)
participant V as Victim's browser
Note over AT,Q: Preparation phase
AT->>Q: Post the review 15 times<br/>(skip HTML using comment markers)
Note over Q: Save the posted strings<br/>as-is<br/>(this save step itself is correct)
Note over V,Q: Victim opens Page V
V->>Q: GET Page V
Note over Q: The vulnerability is here -<br/>embeds the saved title into HTML<br/>without escaping
Q-->>V: HTML containing the attack script
Note over V: Executes as a script element
V->>Q: GET /user/profile
Q-->>V: HTML containing the token
Note over V: Reads document.cookie<br/>and makes it the contents of a.png
V->>Q: POST /user/upload<br/>(uploadfile=a.png, token=…)
Note over Q: Saves without checking format,<br/>publishes it as the avatar image
Note over AT,Q: Retrieval
AT->>Q: GET the victim's avatar image
Q-->>AT: File with the session ID written in it
Egress Controls Don’t Stop This
The awkward part of this route becomes clear once you diagram it: the victim’s browser communicates with nothing but the legitimate site (example.jp), start to finish.
As a result, every countermeasure that works by watching network traffic misses entirely. Only the last one in the following table actually works.
| Countermeasure | Against this attack | Reason |
|---|---|---|
| Proxy URL filtering | Doesn’t work | The only destination accessed is a legitimately business-relevant e-commerce site |
| Firewall monitoring of outbound traffic | Doesn’t work | No communication to an unfamiliar domain ever occurs |
Content Security Policy’s connect-src restriction (a directive such as 'self' that allows the same origin) |
Doesn’t work | Both of the two network calls target the same origin, staying within what the policy allows |
Content Security Policy’s script-src restriction (a configuration that does not allow 'unsafe-inline') |
Works | The embedded inline script never executes in the first place |
When people think of exfiltration via XSS, they tend to picture “the cookie gets sent to the attacker’s server,” but this problem shows clearly that the exfiltration destination can be the victim site itself. Anywhere on the site that is both writable and readable becomes a hand-off point — a file-upload feature, a public profile field, or a public notes field are typical examples.
A note on CSP: among the directives commonly used in practice, the one that reliably stops this attack is not connect-src (which restricts communication destinations) but script-src. With script-src 'self' specified and 'unsafe-inline' not allowed, the inline <script> embedded in the review simply never executes. If you understand CSP only as “something that narrows outbound communication,” you’ll miss this distinction.
That doesn’t mean connect-src is powerless in principle. A directive like connect-src 'none', which forbids even same-origin communication, or an allow-list narrowed to specific endpoints, would also stop these two XHR calls. But few sites can lock things down that tightly, so as a practical defence, script-src is the one to rely on. The accurate way to put it is not “same-origin traffic is outside CSP’s scope,” but rather “a typical directive that allows the same origin lets this 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 retrieved information. The model answer is: “Impersonate the member who accessed Page V and use Web App Q’s functions.”
According to the grading commentary, the correct-answer rate for this question was high; it notes that the impact of an attacker obtaining a cookie on an e-commerce site was well understood.
And re-reading the problem statement’s specification, the member profile feature includes a page for registering credit card information, explicitly stated as available only to logged-in members. The problem statement had already set up, from the start, exactly what lies at the end of impersonation.
8. Why It Doesn’t Work From the Attacker’s Own Site — Question 4
Question 4 is the most essential question in this whole item.
Suppose HTML containing the same script as in Figure 4 is prepared on a site at a domain the attacker controls, and a logged-in member of Web App Q accesses that site. Even so, the attack does not succeed, owing to a mechanism of the web browser. Describe this mechanism in 40 characters or fewer.
The model answer is: “The mechanism by which cookies are not sent from a script to a URL on a different domain.”
Suppose the same script is placed at evil.example and a Company Q member is lured into visiting it. What happens?
First, the initial communication misses. When the script on evil.example sends an XMLHttpRequest to https://example.jp/user/profile, this is a cross-origin request.
The decisive point is that no cookie is attached. The script in Figure 4 does not specify 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 it does not return a profile settings page containing the token. The script cannot obtain the token, and stops right there.
There’s another wall, too: the response cannot be read. If Company Q does not allow evil.example via Access-Control-Allow-Origin, the browser’s CORS check fails, the browser treats the communication as a network error, onload never fires, and xhr.response is never reached. An ordinary site does not return this header for an unfamiliar origin, so in practice this should stop the attack here too.
That said, the problem statement doesn’t say how Company Q configured CORS. Even if access from evil.example were allowed, what could be read would only be the unauthenticated response, which doesn’t contain the token, so the attack still doesn’t succeed. The first point alone — no cookie attached — already stops the attack.
Either one on its own would stop the script, but in fact both are in effect.
On top of that, the cookie itself cannot even 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 targets the first of these — namely, that “cookies are not sent to a URL on a different domain” — as the answer.
Please don’t remember these two as the same mechanism. They are separate mechanisms.
- The reason
document.cookiedoes not returnexample.jp’s cookies is that cookies are managed separately per domain. For separate sites (separate registrable domains) such asevil.exampleandexample.jp, this separation cannot be loosened by configuration. Subdomains under the same domain are a different matter, though — using a cookie’sDomainattribute lets you share cookies betweensub.example.jpandexample.jp7 - The reason no cookie is attached to a cross-origin XHR is that
withCredentialsdefaults tofalse. This one can change if the right conditions line up: if the script specifieswithCredentials = true, and the cookie’sSameSiteattribute allows cross-site sending, and the server returns anAccess-Control-Allow-Originthat explicitly names the sending origin plusAccess-Control-Allow-Credentials: true, then the cookie is sent, and the response becomes readable too
Keep the reason it actually failed and what would be needed if it were rewritten separate here.
There is exactly one thing you can say for certain from the problem statement and Figure 4: because withCredentials is not specified, no cookie is attached, the request is treated as unauthenticated, and the token cannot be obtained. This alone stops the attack.
The absence of CORS permission is also a wall that keeps the response unreadable, but since Company Q’s configuration isn’t stated in the problem, treat this as a conditional reinforcement — “that’s how an ordinary site would be configured” — rather than a certainty.
So what happens if the attacker rewrites the script to withCredentials = true? Only now does another condition come into play. If the cookie’s SameSite doesn’t allow cross-site sending, then even with withCredentials set, that cookie still won’t be attached. Modern browsers treat a cookie with no SameSite specified as Lax, so by default the outcome falls on the side of not attaching it.
Note that how SameSite was set on Company Q’s session-ID cookie is not stated in the problem. So it can’t be said that “it was protected thanks to SameSite.” This is only a statement that, against an attacker who comes in with credentials, SameSite can be effective.
To sum up: reading an authenticated response from a different domain requires all three of the following to line up: withCredentials = true; the cookie’s SameSite allowing cross-site sending; and the server allowing the sending origin via credentials-enabled CORS. Being on a different domain doesn’t guarantee safety, but neither is it broken by satisfying just one condition. When auditing your own site, don’t just take comfort in cookie domain separation — you need to actually check the CORS configuration (in particular, which origins are allowed for credentials-enabled requests) and the cookie’s SameSite attribute.
Why Stored XSS Is So Valuable
What this question teaches us is why an attacker insists on “running their own code inside the victim site.”
Even if the attacker prepares a look-alike fake site on their own domain, the browser treats it as a separate site. They cannot read the target site’s cookies via document.cookie, and with an XHR sent without credentials, as in this question’s script, they don’t even register as logged in from the target site’s point of view. The value to an attacker lies precisely in the code running on the victim site’s origin. Stored XSS is exactly the means of achieving that.
But don’t read this as “an authenticated request can never be sent at all from a different domain.” If the cookie’s SameSite setting allows cross-site sending (such as SameSite=None), a malicious page can still get a request carrying the cookie delivered to the target server. A form POST is the classic example: what CORS mainly controls is “whether the script can read the response,” not “whether the request reaches the server” (though when a custom header is attached, or the request is sent as application/json, a preflight request runs first, and if that isn’t allowed, the actual request is never sent at all). This is why the attack called CSRF is possible in the first place, and exactly why a CSRF-protection token is needed.
In other words, what a cross-origin script cannot do by default is read the target’s cookies and read the response — not send a request.
What’s more, these two kinds of “can’t read” differ in strength. The domain separation of document.cookie cannot be loosened by any server-side configuration, but whether the response can be read depends on the target server’s CORS configuration. If the server allows the sending origin (returning a specific origin in Access-Control-Allow-Origin, together with Access-Control-Allow-Credentials: true for credentialed requests), then even a cross-origin script can read the response. This is exactly why you should check your own site’s CORS configuration.
What makes XSS special is that it leaps over all of these conditions and gets to behave as the victim site’s origin from the very start.
Put differently, XSS is not “a bug where strange characters show up on screen” — it is a bug that puts the attacker in the exact same position as a legitimate user of your own site. This difference in understanding is what determines how you judge priority.
9. Countermeasures That Worked, and Ones That Didn’t
Everything so far is summarized in a single table below. This is the part of the article we most want you to take away.
| Mechanism Company Q did/did not have | Against this attack | Reason |
|---|---|---|
| Communication over HTTPS | Didn’t work | Protects the communication channel; unrelated to a script embedded in the page |
| The 50-character review title / 300-character detail input limit | Didn’t work | Posted in 15 split parts, with the HTML in between skipped over as a JavaScript comment |
| The upload token (a CSRF countermeasure) | Didn’t work | A same-origin script can obtain the token through the legitimate steps |
| The upload feature requiring login | Didn’t work | It’s the logged-in victim’s own browser that runs it |
| Output-time escaping (absent) | Works (fundamental fix) | The posted string stops being interpreted as HTML, so the script never runs in the first place |
| The cookie’s HttpOnly attribute (absent) | Works (fallback countermeasure) | The session ID can no longer be read via document.cookie |
| Checking the format of uploaded files, and re-encoding them (absent) | Works (fallback countermeasure) | Raw text can’t be saved as a PNG, and re-encoding also erases strings hidden in metadata. It survives, though, if encoded into the image’s pixels themselves |
The first four rows above had no effect at all against this attack. Mapping the last 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: the first four rows aren’t “things IPA does not recognize as countermeasures.” IPA’s classification has just two categories, fundamental fix and fallback countermeasure, and checking the content of input values is in fact listed as a fallback countermeasure. It comes with the caveat, though, that “this countermeasure’s effectiveness is limited” — and this case is exactly an instance of that limitation.
And the important point is that if even one of the last three had been present, the attack chain executed in this problem would have broken somewhere.
- If escaping were in place, the script would never execute at all
- If HttpOnly were set, the script would execute but couldn’t read the cookie
- If the format were checked, the cookie could be read, but raw text couldn’t be uploaded as a PNG
This is exactly the kind of situation where defence in depth matters. That said, fallback countermeasures are not a substitute for the fundamental fix. Even with HttpOnly set, if the XSS remains, the attacker can still carry out arbitrary operations in the victim’s browser — buying products, changing registered information, and so on. Attacks that don’t even need to steal the session ID can be designed without limit.
Why the Table Says “Format Check and Re-Encoding”, Not Just “Format Check”
There’s a reason the last row of the table is written as two steps, “format check and re-encoding.”
In the problem statement, Company Q didn’t check the image file’s format at all, so adding a format check alone would stop this attack. But that’s only true when the attacker sends raw text. An attacker could just as well build a legitimate PNG file and slip a string into its metadata region. In that case, a “is the format correct” check would pass right through.
So to narrow this route down, you need to go as far as re-encoding the uploaded image server-side before serving it (not saving the original byte sequence as-is).
Even re-encoding, though, doesn’t close it off completely. If the attacker builds a legitimate PNG by drawing the session ID as the pixels themselves, ordinary re-encoding preserves that visual information, so the attacker can still download it and read it back out. What re-encoding stops is the variant that sends a raw byte sequence and the variant that hides it in a metadata region — not information encoded into the visible content of the image.
In short, as long as a location that is both writable and readable exists on the site, it can never be fully closed off as an exfiltration route. This captures the nature of fallback countermeasures well. It isn’t enough for a fallback countermeasure to stop “the attack observed right now” — it needs to be designed while considering whether it still holds up if the attacker changes their technique slightly. And no matter how many you stack up, they never substitute for the fundamental fix: output-time escaping.
One more note: a design that serves uploaded files from a separate domain from the main site is often recommended, but it is not a countermeasure for this particular exfiltration route. An attacker can download the image from a different domain just as easily, because the same-origin policy is not a mechanism for keeping a publicly served byte sequence secret. Where serving from a separate domain does help is against the threat of an uploaded file itself being executed as active content on the victim site’s origin (the kind of thing that happens when HTML or SVG gets uploaded). Understand it as a countermeasure for a different threat, and keep the two separate.
The idea of judging a file by its actual contents rather than by the sender’s claim (its extension or Content-Type) is a basic practice needed well beyond the web. But what “judging by contents” means differs by format. For a format like PNG, which has a fixed byte sequence at the start, you can check that. CSV, on the other hand, has no standardized signature; a leading BOM only indicates the character encoding and is no proof that the content is actually 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 the check instead comes down to whether it parses successfully under the expected dialect, and whether it stays within the expected schema and row-count limit.
10. What to Check in Your Own Code
Turning this whole question into a code-review checklist gives you the following. Any web application with member accounts or a posting feature can use it as-is.
- Is output-time escaping in effect at every output point? Enumerate every place where the template engine’s automatic escaping has been explicitly disabled (
raw,| safe,dangerouslySetInnerHTML, and so on), and check whether you can explain, for each one, the reason it’s justified to disable it - Are input-side restrictions being counted as an XSS countermeasure? Character limits and restrictions on allowed character types are specification requirements, not the fundamental fix for XSS
- Is escaping happening on input? The input side’s role is validation that rejects out-of-specification values, not escaping. At input time, the destination (HTML, CSV, JSON, email, log) isn’t yet decided, so escaping on input invites both double escaping and data corruption. The same goes for building SQL statements — the fundamental fix there is placeholders
- Do cookies have
HttpOnly,Secure, andSameSiteset? Check the session ID’s cookie in particular - Is the format of uploaded files checked by their actual contents, rather than by extension or Content-Type? Information declared by the sender is not valid grounds for verification. Note that a format check alone still lets through the trick of hiding data in the metadata region of a legitimate image
- Are uploaded images re-encoded before being served? Check that the original byte sequence isn’t being returned as-is. Bear in mind, though, that information encoded as pixels survives even re-encoding, so understand that a “writable and readable” location can never be fully closed off. Serving from a separate domain, meanwhile, is a countermeasure against the threat of an uploaded file being executed on your own site’s origin — not against this exfiltration route
- Is the CSP configured to block inline scripts? The criterion is not “does
'unsafe-inline'appear” but “is an inline script actually allowed.” Whenscript-srcspecifies a nonce or a hash, browsers supporting CSP Level 2 and later ignore'unsafe-inline', so under a backward-compatible policy that writes both'unsafe-inline'and a nonce together, an injected script without the nonce is still blocked - Can you explain what a token actually protects? A CSRF-protection token does not prevent XSS. Each needs its own separate countermeasure
Item 1 in particular is a typical pattern that turns up in real-world investigations. It’s not unusual to find a case where the team assumed the framework’s automatic escaping kept things safe, but had disabled that automatic escaping in just one place to meet a one-off requirement to “insert raw HTML.”
Closing — The Real Skill This Question Is Testing
The list of countermeasures covered so far is summarized at the top, in “The Bottom Line First.” To close, here’s one last thing worth saying about this question itself.
What makes this a well-designed exam question is that it doesn’t ask “do you know what XSS is?” — it asks “can you tell, among the countermeasures already in place, which ones are actually working?”
Company Q wasn’t doing nothing. It communicated over HTTPS, set a character limit on input, required a token for uploads, and restricted the feature to logged-in members. Lined up, it looks like a build with a reasonable amount of protection. And yet every single one of them was slipped past. The three things it lacked, by contrast, are all unglamorous — none of them is the kind of feature you’d write up in a release note.
What makes security discussions hard is that the number of countermeasures and the strength of the defence are not proportional. What matters is not “what are we doing” but whether you can state, one by one, “which attack, and which stage of it, does this countermeasure stop.” This isn’t a skill needed only for a certification exam — it’s the judgement most needed both by whoever is receiving an explanation of security measures and by whoever is implementing them.
Related Consulting Areas
KomuraSoft LLC handles both the development of websites with member accounts and posting features, and design reviews to check whether an existing web application has the same hole open.
- Website Development and Renewal
- Technical Consulting & Design Review
- Bug Investigation & Root Cause Analysis
- Contact
References
-
IPA (Information-technology Promotion Agency, Japan), the “Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5), Afternoon, Question” contained in Exam booklets, scoring weights, model answers, and grading commentary (fiscal 2023, Reiwa 5). On the features of Company Q’s Web App Q (member registration, login and session-ID issuance via cookie, the product review feature’s 50-character review-title limit and 300-character review-detail limit, the member profile feature’s avatar image upload and credit card information registration, the avatar image upload taking an image file and a token as parameters, and an uploaded avatar image being displayed on the member profile settings page and on review pages); on the phenomenon of Page V showing only 2 of 16 reviews; on the HTML of Page V containing 15 posts by Member A along with a long script; on the content of the extracted script; and on the vulnerability in Web App Q whereby a script entered by a member gets executed, the absence of the HttpOnly attribute on the cookie, and the absence of a format check on uploaded image files. The text of Questions 1 through 4 is also drawn from this booklet. ↩ ↩2
-
IPA (Information-technology Promotion Agency, Japan), Frequently Asked Questions About the Examinations. On the use of past exam questions published by IPA not requiring permission or a usage fee except where specifically provided for by law, but IPA not having waived copyright; on the requirement to state the source in the form “fiscal year, session, examination category, time slot, question number, etc.” (the example given is “Source: Fundamental Information Technology Engineer Examination, Spring 2019 (Heisei 31), Morning, Question 1”); and on the requirement to also note it explicitly whenever part of a question has been modified. ↩
-
IPA (Information-technology Promotion Agency, Japan), Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5), Model Answers. On the intent of Question 1 (testing the ability to read, from an incident-response case study involving the exploitation of a vulnerability in a web application program, which vulnerabilities and problems in the HTML and ECMAScript were exploited, and to devise countermeasures), and on the model answer for each question (Question 1(1): “Stored XSS” — choice イ in the booklet; Question 1(2): “Apply escaping to the review title before it is output.”; Question 2: “Posted multiple times in a way that commented out the HTML so it became a single script.”; Question 3(1): “Uploads the session ID as an avatar image, together with the token obtained from the XHR response.”; Question 3(2): “Downloads the member’s avatar image and extracts the session ID string from it.”; Question 3(3): “Impersonate the member who accessed Page V and use Web App Q’s functions.”; Question 4: “The mechanism by which cookies are not sent from a script to a URL on a different domain.”). ↩ ↩2
-
IPA (Information-technology Promotion Agency, Japan), Registered Information Security Specialist Examination, Autumn 2023 (Reiwa 5), Grading Commentary. On the overall correct-answer rate for Question 1 being about average; on the note for Question 1(1) that “the correct-answer rate was about average, but a number of candidates were seen to answer incorrectly with ‘DOM Based XSS’, apparently because the script used the DOM”; on the remark that candidates should understand vulnerabilities accurately, including their characteristics and countermeasures; on the note for Question 2 that “some answers were seen that appeared to reflect insufficient checking, such as ‘removed the input restriction using developer tools before posting’”; on the remark that candidates should develop the ability to carefully check the traces an attacker leaves behind and accurately grasp the attack method; and on the correct-answer rate for Question 3(3) being high, with the impact of an attacker obtaining a cookie on an e-commerce site being well understood. ↩ ↩2
-
IPA (Information-technology Promotion Agency, Japan), “How to Secure Your Website”. On presenting the threats and countermeasures for 11 types of website vulnerability, divided into “fundamental fix” (an implementation that removes the cause of the vulnerability itself) and “fallback countermeasure” (a measure that lowers the success rate or impact of an attack when the vulnerability remains); on listing, as the fundamental fix for cross-site scripting, applying escaping to every element output to the web page; and on the accompanying security implementation checklist and the separate volumes “How to Call SQL Safely” and “Web Health Check Specification.” ↩ ↩2 ↩3
-
IPA (Information-technology Promotion Agency, Japan), “How to Secure Your Website” — 1.1 SQL Injection. On “implement all SQL statement construction using placeholders” being listed as the fundamental fix for SQL injection, with static placeholders (prepared statements) said to be incapable, in principle, of producing the vulnerability; on the wording given for the implementation used when SQL statements must be built by string concatenation — “when SQL statement construction is done by string concatenation, use the database engine’s API that performs escaping etc. to correctly form the literals of the SQL statement” — meaning that escaping is performed on the construction of the SQL statement’s literals, not at the point input is received; and, alongside this, on “do not specify SQL statements directly in parameters passed to the web application” being listed as a further fundamental fix, and “do not display error messages as-is in the browser” and “grant the database account only the appropriate privileges” being listed as fallback countermeasures. ↩
-
IETF, RFC 6265: HTTP State Management Mechanism, Section 4.1.2.6, “The HttpOnly Attribute.” On the HttpOnly attribute limiting the scope of a cookie to HTTP requests, in particular instructing 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 Secure attribute (Section 4.1.2.5) restricting a cookie to being sent only over a secure channel. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Registered Information Security Specialist Exam, Spring 2024 (Reiwa 6) PM Q1 Commentary — JWT alg=none, API Authorization, and Interim WAF Mitigation
Using the Registered Information Security Specialist Examination's Spring 2024 (Reiwa 6) PM Q1 as a case study, this article explains JWT...
What Website Clients Should Know Too — Using IPA's 'How to Secure Your Website' as a Checklist
What standard should you use to check your company website's security against? This article explains the 11 vulnerabilities and counterme...
Registered Information Security Specialist Exam — Autumn 2023 (Reiwa 5) Afternoon Question 2 Explained — Files Walking Out Over the Guest Wi-Fi
Using Question 2 of the afternoon session of the Autumn 2023 (Reiwa 5) Registered Information Security Specialist exam, this article expl...
Information Security 10 Major Threats 2026 — How to Read the Ranking, and What SMEs Should Actually Guard Against
In IPA's 'Information Security 10 Major Threats 2026,' ransomware attacks took first place for the 11th year running, supply chain attack...
Where Should SMEs Start on Security? — A Walkthrough of IPA's 'Information Security Guidelines for SMEs,' 4th Edition
Where should small and medium-sized businesses start on security? Drawing on IPA's 'Information Security Guidelines for Small and Medium ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Website Development
For websites with member accounts and posting features, the issues in this article — output-time escaping, setting cookie attributes — translate directly into build quality.
Technical Consulting & Design Review
Auditing an existing web application for the same kind of hole, from a design-review perspective, is exactly the sort of work our technical consulting covers.
Frequently Asked Questions
Common questions about the topic of this article.
- Why is the XSS in this question 'stored' when the script manipulates the DOM? Isn't that DOM Based XSS?
- The type of vulnerability is decided by *where* the attacker's string turns into executable code — that is, 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 HTML and returned it as-is. The sink is in the server's output processing, and the string is saved and then distributed to everyone who subsequently opens the page — that makes it stored XSS. DOM Based XSS refers to the category where browser-side JavaScript makes a value executable, for example by assigning it to innerHTML. Whether the embedded script happens to use DOM APIs such as XMLHttpRequest or getElementById has nothing to do with the vulnerability type. Note, too, that even if the server returns a saved value as harmless text or JSON, it becomes DOM Based XSS the moment browser-side JavaScript passes it to innerHTML. Judge by where it became executable, not by whether the string is visible in the response. IPA's own grading commentary notes that a number of candidates were seen to answer incorrectly with DOM Based XSS, apparently because the script used the DOM.
- There was a 50-character input limit on the review title. Why could a long script still execute?
- The attacker split the payload into lengths that fit within a single post and posted it across multiple posts. The first post opens a script tag partway through the title and opens a JavaScript block comment 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 this pattern repeats. Doing this makes all the HTML sitting between posts (div tags and the like) fall inside a JavaScript comment, so it's ignored, and only the split fragments join together to run as a single script. The character limit only restricted the length of each individual post — it never restricted the number of posts.
- Where was the stolen session ID sent?
- It was never sent to an external server. The script turned the contents of the cookie directly into the contents of a file, named it "a.png", set its MIME type to "image/png", and sent it to the very avatar-image upload feature of the e-commerce site the victim was using. Because the uploaded avatar image is displayed on pages such as the review page, the attacker could simply download that image like anyone else and pull the session ID out of its contents. The victim's browser never communicated with anything but the legitimate site, so no suspicious external traffic was ever generated — a route that proxy URL filtering and firewall monitoring of outbound traffic cannot catch.
- The upload was supposed to require a token. Why did the attack still succeed?
- That token is a mechanism for rejecting unauthorized requests from a different site (a CSRF countermeasure) — it was never designed with a script running on the same site in mind. The attack script first accessed the profile settings page via XMLHttpRequest, obtained the token the same way the legitimate screen does, and then carried out the upload with that token attached. Because XSS lets 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've heard that escaping on input matters for SQL injection. Is output-time escaping unique to XSS?
- No — for SQL injection too, "escaping on input" is not the countermeasure. What IPA lists as the fundamental fix is "implement all SQL statement construction using placeholders." Escaping is mentioned as an alternative for when SQL statements are built by string concatenation, but even there it's for "correctly forming the literals of the SQL statement" — that is, it happens at the moment the SQL statement is constructed, not when the input is received. The shared principle is: escaping is done at the point where it's decided which grammar the data is heading out into. If it's going out as HTML, use HTML escaping; if it's becoming part of a SQL statement, use placeholders; if it's becoming part of a shell command, handle it the shell's way. The reason you must not escape on input is that, at that point, the destination isn't decided yet: the same data might go out into an HTML page, a CSV, JSON, a notification email, or a log. Applying HTML escaping at input time makes entity references show up verbatim in a CSV, or leaves the database holding something different from the original input, throwing off search and aggregation. This doesn't mean the input side should do nothing — its role is validation, not escaping: rejecting values that are impossible under the specification.
- What practical takeaway should I bring back from this question?
- The fundamental fix is to apply escaping, immediately before output to HTML, to every piece of user input — including the review title. On top of that, as fallback countermeasures: set the HttpOnly attribute on the session-ID cookie so scripts cannot read it; re-encode uploaded images server-side so the original byte sequence is never preserved; and configure Content Security Policy so it can stop inline script execution. If Company Q in this question had implemented even one of these, the attack chain would have broken somewhere. Bear in mind, though, that fallback countermeasures can be circumvented if an attacker changes technique, so they are never a substitute for the fundamental fix of escaping.