A CMS editor form crosses several trust boundaries. It accepts a request from a browser, changes stored records, and later displays content to readers or administrators. XSS, CSRF, and SQL injection affect different parts of that journey. A control that addresses one boundary should not be treated as a universal security switch.
This guide uses a content-editing workflow as an audit example. The practices apply to the application's own code, themes, extensions, and integrations. Verify the behavior of your framework and configuration, and test only systems you are authorized to assess.
Map the data flow first
Choose one state-changing route, such as saving a draft, and document:
- How the user is authenticated and which role may perform the action.
- Which fields are accepted and where their values originate.
- How the request is protected against cross-site submission when browser credentials are sent automatically.
- Which database operations are performed and how values are passed to them.
- Every location where the saved content is later rendered, including previews, dashboards, notifications, and exports.
Add public comments, search parameters, imports, and integration responses to the same inventory. Data does not become trusted merely because it was stored in the database or delivered by another service.
XSS: treat output as a context-specific operation
Cross-site scripting can occur when untrusted data is interpreted as active code in a page rather than as the intended data. A saved comment displayed to an administrator can be as relevant to the audit as the public article that displays it.
For ordinary HTML text, use your templating system's automatic output encoding. If you deliberately allow formatted HTML, use a maintained HTML sanitizer with a carefully configured allowlist for elements, attributes, and URL schemes. A short regular expression is not a replacement for parsing and sanitizing HTML.
In PHP, this minimal example encodes a string for HTML text:
<?php
$comment = 'A < B & C';
echo htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
This is an HTML output example, not an encoder for every context. A value placed in JavaScript, CSS, or a URL needs handling appropriate to that context. Avoid interpolating untrusted input into executable script or event-handler attributes. For links, validate the allowed scheme and intended destination separately; HTML encoding alone does not make an arbitrary URL safe.
On the client, prefer APIs that set text when the content is text, rather than APIs that parse it as HTML. Search for direct HTML insertion in theme code and plugins. A Content Security Policy can provide defense in depth, but it does not replace correct encoding or sanitization. Likewise, HttpOnly and Secure cookies do not make an XSS flaw harmless: code running in the page may still use actions available to the user.
CSRF: verify state-changing requests and permissions
A cross-site request forgery risk exists when a browser automatically attaches a user's credentials to a request and another site can induce an unwanted action. An authenticated session alone does not prove that the user intended a particular state change.
For cookie-authenticated browser workflows, use the framework's established CSRF protection. A typical server-side design issues an unpredictable token associated with the user's session, includes it in the form or supported request mechanism, and rejects an absent or invalid token before changing state. Avoid exposing tokens in URLs and logs.
Then perform a separate authorization check: a valid token doesn't give an editor permission to modify every object. Check the user's role and access to the specific record on the server. Protect asynchronous requests as well as traditional forms when they use the same credential model, and avoid using GET for state-changing operations.
SameSite cookie settings, Origin or Referer checks, and reauthentication for particularly sensitive actions can add layers, subject to the needs of the application. Do not assume that JSON formatting or CORS alone resolves CSRF. Determine whether credentials are automatically attached and select controls appropriate to the endpoint. Separately authenticated webhooks need their own sender verification. An XSS issue can also undermine CSRF protections, so these controls must be designed together.
SQL injection: keep values separate from query structure
When a request value is concatenated into SQL text, the boundary between data and query syntax can be lost. Use the database library's parameterized queries and pass values as parameters. Review administrative filters, search, reports, scheduled jobs, and extensions, not just the login path.
Validation is still useful for business rules: an identifier can be checked for type and range, for example. It is not a substitute for parameterizing the query. Review the actual data-access layer, including whether the chosen library and driver handle the selected query safely.
Placeholders generally represent values, not arbitrary table names, column names, or sorting directions. For user-selectable sorting, map an input key to a fixed allowlist of known identifiers and directions before constructing that part of a query. For lists of values, use the library's supported method for binding the required number of parameters.
Run the application using a database account with only the permissions it needs. Keep detailed SQL errors and stack traces out of public responses, while retaining protected diagnostics without credentials or other secrets. Reduced privileges limit potential damage but do not repair unsafe query construction.
Check the controls together on a test environment
Use a dedicated environment with fictitious data and a rollback plan. Then turn the data-flow map into repeatable checks:
- Rendering: untrusted text appears as text in each intended context; allowed rich content is sanitized; link destinations obey the configured policy.
- Authorization: an authenticated user cannot edit another user's restricted record merely by changing an identifier.
- CSRF: missing or invalid tokens are rejected for cookie-authenticated state changes; a valid token does not bypass the object-level permission check.
- Database: values are bound as parameters, and any dynamic identifiers come from a fixed allowlist.
- Extensions: custom themes, plugins, preview pages, and background jobs use the same security boundaries.
- Regression: add automated checks for each fixed defect and review dependencies as part of maintenance.
An automated test suite is valuable evidence, not proof that every vulnerability has been eliminated. If the application has significant exposure or complex extensions, plan an independent review with access and scope agreed in advance.
Operational controls matter too
Keep an inventory of the core, themes, extensions, and runtime. Assign ownership for security updates and compatibility testing. Give people individual accounts and the least access needed for their roles; remove access that is no longer needed. Keep secrets and backups outside public paths and control who can read them.
Backups must be restorable. Test restoration in isolation and include both data and required configuration in the recovery plan. If compromise is suspected, restrict access and preserve useful evidence, assess the scope, rotate affected credentials, and recover a trusted version after addressing the underlying cause. Restoring a backup without fixing the entry point can reintroduce the same risk.
The most productive first step is to follow one important form from browser to database and back to the page. Assign a control to each boundary, verify permissions for the specific object, and record the tests that will catch a regression.
Original Russian article: https://morlix.ru/blog/bezopasnost-cms-zashchita-ot-xss-csrf-i-sql-inektsiy
Top comments (0)