Web Application Security: XSS, SQL Injection, and CSRF

What You Will Take Away from This Article

  • Encode outputs according to their context.
  • Use parameterized queries instead of concatenating text in SQL.
  • Protect session-dependent requests from CSRF.

XSS: When Text Turns into Code

Cross-Site Scripting occurs when an attacker can inject content that is interpreted as executable instructions within a page that the user trusts. This can exploit available script data or execute actions within the session's permissions. Scenarios vary between Stored, Reflected, and DOM-based XSS. Encoding outputs must suit the context: HTML text, attribute, URL, or JavaScript. Use textContent for literal text and avoid inserting untrusted data via innerHTML. CSP can add defense, but it does not replace proper source handling and encoding.

SQL Injection: Separate Data from Queries

SQL Injection appears when user inputs are integrated into SQL text, which can be interpreted as part of the query language and change its meaning. The basic solution is parameterized queries or prepared statements that separate values from the query structure. Simply removing quotation marks or banning specific words is not enough; blacklists are fragile. Table names, columns, and parts of ORDER BY often need a static allowlist when chosen dynamically. Grant the database account the least necessary permissions and do not use an administrative account for the site's daily requests.

CSRF: A Request from a Logged-In Browser

Cross-Site Request Forgery exploits a browser that automatically carries credentials, such as a session cookie, to send a request that the user did not intend. Use CSRF tokens in state-changing operations where necessary, set SameSite for cookies, and verify Origin or Referer according to the threat model. Do not make GET change or delete data. SameSite is an additional defense and not a replacement for proper authentication design, especially when using more than one domain or complex login flows.

Authentication, Permissions, and Security Testing

Authentication answers who the user is, and authorization determines what they can do. Verify permission on the server for each operation and resource, not just when displaying a button. Enable HTTPS, limit login attempts, protect account recovery, and do not log passwords or secrets. Test inputs, outputs, and state changes in an environment you are authorized to test, then add testing that prevents the repetition of the root cause of each vulnerability.

Parameterized Query Instead of Input Concatenation

const result = await db.query(
  "SELECT id, email FROM users WHERE id = $1",
  [userId]
);

// Do not build SQL by concatenating userInput inside the text.

Draw Trust Boundaries in the Application

Many vulnerabilities start when the application treats a value coming from the user as if it were trustworthy. Search text must remain text when displayed, and an email value must not change the structure of SQL, and a request sent by a browser carrying a session must not be automatically accepted just because the cookie is present. These are different boundaries and require different controls.

Proper encoding when outputting text prevents its interpretation as HTML tags in the correct context, parameterized queries separate user data from SQL syntax, and CSRF controls verify that the intended request came from a trusted flow. There is no single sanitization function that fits all these contexts; the choice of control depends on how data is used.

Detailed Case Study: From Display to Root Cause

The practical case here is: a page displaying user input and a database query, with a sensitive request depending on the browser's session cookie. It's not enough to just fix the apparent symptom; we need to draw the data or request path from its beginning to the outcome and understand where the discrepancy originated. Start by logging what happened, what was expected to happen, and the step that can be proven by measurement, log, or test. After that, change one clear factor, then redo the scenario to verify that the root cause was addressed. If several components changed at once, it will be hard to know which one made the difference.

Link the case outcome to the article's principle: XSS: when text turns into code and SQL Injection: separate data from queries. These concepts define what should be checked first and what can be postponed. Also, record cases that were not tested, so that the success of one scenario does not become a claim that the entire system is problem-free.

Defense in Layers, Not a Single Sanitization

For XSS, use output interfaces that escape text in the appropriate context and avoid inserting raw HTML except when necessary and with a trusted sanitization library. For SQL Injection, use bound parameters instead of concatenating text, and remember that column names or sorting directions do not always accept parameter values; these parts need a specific allowlist in the code.

CSRF relates to requests that depend on credentials automatically sent by the browser, such as a session cookie. Use CSRF tokens when needed, appropriate SameSite policy, and verification of Origin or Referer according to the application design. These controls do not address XSS instead of it; if an attacker can run JavaScript within the same origin, some CSRF defenses become less effective.

From Example to Implementation in a Real Project

Start from the code example presented in the article: "parameterized query instead of input concatenation". Do not move it literally to production before defining its inputs, outputs, and assumptions. Ask what happens when a value is missing, or unexpected data arrives, or the request is repeated, or external authentication is delayed. After that, implement a small version within an independent branch and add a test for the basic case and another for a failure case.

Use these steps as a beginning: draw trust boundaries between the browser, server, and database. Then verify inputs on the server. Then use output encoding and secure DOM interfaces. Make each step reviewable, and do not combine project settings and secrets within an educational example. If your environment differs from the example, document the difference and adjust the implementation based on official documentation rather than copying the solution without understanding.

How to Verify the Fix Without Causing Harm

Start by reproducing the flaw in a testing environment, identify the input controlled by the user, the layer interpreting it, and the visible outcome. After the fix, add a test that prevents the problem from returning. Do not test just one payload; test empty inputs, long inputs, special characters, and encoding cases.

When reviewing SQL injection, verify that the value does not change the query structure and that the query still uses bound parameters. And when reviewing XSS, test in the context of text, attribute, and link if all are used. Do not send test payloads to systems you do not have permission to test, and do not rely solely on automated scanning tools to decide that the application is secure.

Systematic Diagnosis Approach Upon Failure

When the solution does not work, do not start adding another condition before identifying the evidence of failure. Record the input, the expected outcome, the actual outcome, and the message or measurement that supports the conclusion. One of the repeated mistakes in this topic is relying on validation in the browser alone. Treat this point as a hypothesis for testing, not as a preconceived diagnosis; the problem's cause might be in an adjacent layer or in an incorrect assumption during testing.

Divide the system into clear boundaries: inputs, processing, external authentication, and outcome. Test each boundary separately as much as possible, then implement an integrated scenario. If you fix the issue, add a test that prevents its return or an operational observation that helps in early detection. The goal is for the diagnosis to become repeatable by another team member, not to remain tied to the memory of the person who discovered the fault.

Security Extends to Settings and Logs

Even if you handle inputs correctly, there might still be issues in secret management, updating dependencies, excessive permissions, or error messages that reveal the system's structure. Store keys outside the repository, reduce permissions, set limits and timeouts for requests, and keep logs that are sufficient for investigation without storing passwords or session tokens.

Separate privileges from authentication: successful login does not mean a user is authorized to read another's log. Apply authorization on the server for every sensitive operation, log unusual access attempts, and make security library updates part of the maintenance cycle. Also, plan how to invalidate sessions when a compromised account is suspected.

Advanced considerations before adopting a solution

In a real project, a solution is not evaluated on the happy case alone. Review behavior under load, failure cases, future changes, and relevant security boundaries. It is crucial here to separate XSS handling from SQL Injection and CSRF, and review authorization, secrets, and logs as independent controls. Do not treat these points as an afterthought; design decisions can be hard to change when data accumulates or multiple clients depend on them.

Define acceptance metrics or tests before merging, and document the solution's limits clearly. A solution might be suitable for a small site but needs a queue or cache or a stricter architecture as usage grows. Do not add complexity prematurely without evidence, but do not ignore a clear sign that requirements exceed the simple version. A good decision balances current simplicity with the expected cost of change and maintains system clarity for the team.

Make every vulnerability a permanent test

Effective fixing does not end with closing the ticket. Document the root cause, add an automated test or code review check, and identify if other places use the same insecure pattern. If the cause is the adoption of a dangerous interface, it can be wrapped inside a secure component to reduce the recurrence of misuse.

Review the entire attack journey: can the value be injected? Where is it stored? Where is it displayed? What privilege allows the impact? This prevents treating one point while leaving parallel paths open and turns the lesson into a design standard that benefits the whole team.

Practical application and acceptance criteria

Try this exercise on a test project or a separate branch: add a test for each of text display, parameterized query, and sensitive request verification. Write the expected outcome before implementation, record what actually happened, and then fix the difference if it appears. Also, try a boundary or intentional failure case safely. Do not use real data or a production system to cause a test failure.

Consider the task complete when re-test steps are documented, the test proves basic behavior and failure case, there are no secrets or sensitive information in the log, and another developer can understand the reason for the solution. If this is not achieved, what is required is not to add more code directly; rephrase the problem and clarify the assumption that was not correct. This approach makes the article a starting point for a technical practice, not just a theoretical read.

Important questions about web application security

Is input validation enough? No; validation helps with data integrity, but encoding in the context of output and parameterized queries addresses different risks.

Does SameSite always replace the need for a CSRF token? It depends on the application pattern, browsers, and flows; do not assume it covers all cases.

Does automated scanning prove security? No; use it with manual review, authorized tests, and operational controls.

Practical implementation plan

  • Draw trust boundaries between the browser, server, and database.
  • Validate inputs on the server.
  • Use output encoding and secure DOM interfaces.
  • Use parameterized queries and limited privileges.
  • Test authorization, CSRF, and add repeat prevention tests.

Common mistakes to watch out for

  • Relying on validation in the browser alone.
  • Using a blacklist instead of separating data from SQL.
  • Considering login as proof of having privilege.

Conclusion

Security is not one button or one library. Separate data from instructions, encode outputs in their context, protect state-changing requests, and verify privileges on the server every time. Address the root cause and add a test that prevents the recurrence of the vulnerability.

Technical references for further reading