CSRFFilter
The filter offers protection from Cross Site Request Forgery and related attacks by imposing additional conditions on incoming requests to filter out those not originating from the legitimate end user. The following tests are available, listed in the order of execution:
Referer Header
If an incoming HTTP request contains the 'Referer' header, the hostname must match the 'Host' header.
Request ID
JavaScript code with a random session-bound ID is injected into all HTML responses sent to the client. As the page loads in the browser, the script includes the ID in all the references pointing back to the web application. Depending on the request type, the ID is added either as an HTTP header, query parameter or a hidden field in forms. The filter checks incoming requests for the presence of the correct ID and removes it from all the requests passed through. The test should be enabled selectively for the most sensitive requests only, the injected script is not guaranteed to rewrite all the links.
ch::nevis::isiweb4::filter::validation::CSRFFilter
libInputValidationFilter.so.1
+NEEDS_PARAMS
Configuration
RefererHeaderCheck
Type: enum: false
, true
, or required
Usage Constraint: optional, conditional
Default: true
This parameter defines whether the Referer HTTP header check is enabled, disabled or required:
false
: Referer header check is disabled.true
: Referer header check is enabled if the header is present.required
: Blocks the request if the referer header is not present, else validates it.
InjectedIdCheck
Type: boolean
Usage Constraint: optional
Default: false
Secure Default: true
Enables injection of JavaScript in HTML responses as well as the corresponding check on requests.
IdCheckEnable
Type: boolean
Usage Constraint: optional, conditional
Default: true
Supported pragmas: break
With this parameter you can turn on/off the check for the injected Id.
InjectionScriptPath
Type: string
Usage Constraint: optional
Default: /var/opt/nevisproxy/<instance>/conf/csrf-inject.js
Defines the location of the .js
file injected to HTML responses. The provided script (csrf-inject.js
) works only on outgoing AJAX calls and URLs in the <href>
tag.
Some use cases:
- For more complex functions, such as
javascript.window.open()
, copy thecsrf-inject.js
script and adapt it to your backend(s). - To support some basic JavaScript functions, use a copy of the file
csrf-with-basic-javascript-support-csrf-inject.js
. You find this file in theexamples
directory of your installed nevisProxy package. - To support Captcha (or other pages containing image links), use a copy of the file
csrf-with-image-support-csrf-inject.js
. This file is also located in theexamples
directory of your installed nevisProxy package. - To support Content Security Policies where inline scripts are disabled, use a copy of the file
csrf-inject-ext.js
and enableIncludeInjectionScriptExternally
. Note that when the injection script is loaded externally, this parameter is treated as a link to that file. For example whenInjectionScriptPath
is/js/csrf-inject-ext.js
, the html will have the following tag added:<script src="/js/csrf-inject-ext.js"></script>
.
IncludeInjectionScriptExternally
Type: boolean
Usage Constraint: optional
Default: false
Defines how the injection script is added to the html page. When set to false
, the script is added as an inline <script>
block. When set to true
, the script is linked to the page with <script src="InjectionScriptPath"></script>
. The parameter InjectionScriptPath
is treated as an URL when IncludeInjectionScriptExternally
is true
, otherwise it is used as a local file.
The external loading of the injection script is needed when Content Security Policies are configured because secure CSP rules forbid loading inline scripts to mitigate XSS.
The unique request ID and its parameter name is received differently based on the script inclusion mode.
In the inline case, these parameters are set as regular javascript variables. When externally loaded, the parameters are read from <meta>
tags. This makes inline and external injection scripts incompatible with each other so if you used a custom injection script you have to adapt that before switching to external loading mode.
The following changes are needed in the injection script to make it externally loadable:
1.: Read the parameters at the beginning of the script:
var csrfId = document.head.querySelector('meta[name="csrfId"]').content;
var csrfParamName = document.head.querySelector('meta[name="idQueryParam"]').content;
2.: Call csrf*
methods at the end of the script:
document.addEventListener("DOMContentLoaded", (event) => {
csrfModifyLinks(csrfParamName, csrfId);
});
csrfRegisterAjax(csrfParamName, csrfId);
IdQueryParamName
Type: string
Usage Constraint: optional
Default: csrfpId
Name of the extra parameter with the ID added to requests by the injected JavaScript.
ProtectedURIs
Type: list of regexps
Regexp type: PCRE(da)
Usage Constraint: optional
Newline separated list of regular expressions that define request URIs for which an ID is required. All other requests will be allowed. The list should only contain selected sensitive URIs.
IdCheckWhitelist
Type: list of regexps
Regexp type: PCRE(da)
Usage Constraint: optional
Newline separated list of regular expressions that define URIs exempt from ID checks, even if they match ProtectedURIs.
ProtectPayloadOnly
Type: boolean
Usage Constraint: optional
Default: true
Only requires the ID of requests that carry non-trivial payload. All HTTP requests without parameters will be allowed.
ParsingMode
Type: enum: strict
, tolerant
Usage Constraint: optional
Default: strict
In the strict
parsing mode, if there are errors in the HTML response, the whole parsing process is terminated. The tolerant
mode allows forwarding invalid HTML responses from the backend to the client. These responses are not modified by the filter thus reducing security.
ContentTypes.html
Type: list of content type regexps
Regexp type: PCRE(da)
Usage Constraint: optional, advanced
Default:
^text/html
^application/xhtml
Newline separated list of regular expressions defining content types for HTML. If the content type of a response matches one of the configured values, the JavaScript code will be injected.
RewriteBufferSize
Type: integer
Unit: bytes
Usage Constraint: optional, advanced
Default: 16384
The size of the internal buffer for buffering HTML tags. Only relevant if the JavaScript is injected, see InjectedIdCheck
parameter.
CSRFPolicy
Type: space or newline separated list of error policies
Syntax: <status>:<action>[:<error-code>][:<url>]
Usage Constraint: optional, advanced
Default: REFERER_MISMATCH:block:403 ID_MISMATCH:block:403
Defines actions to perform when a particular test fails:
<status>
:REFERER_MISMATCH
,ID_MISMATCH
.<action>
:BLOCK
,REDIRECT
,PASSTHROUGH
,TRACE
<error code>
: HTTP Error code to return, 403 by default (only applies toBLOCK
)<url>
: The destination URL forREDIRECT
(colon characters within the url must be escaped)
RedirectPolicy
Type: enum
Possible values: on, off, protectedUriOnly
Usage Constraint: optional
Default: on
This parameter controls the rewriting of redirect URIs. The default value is on
, which means that the system will overwrite every redirect URI. If you set the parameter to protectedUriOnly
, only the configured (and not whitelisted) URIs will be rewritten. To disable the feature, set the parameter to off
.
RegenerateId
Type: boolean
Usage Constraint: optional
Default: false
If this parameter is set to true
, the system will generate a new ID for each request. If set to false
, which is the default, the existing ID remains in use.
Examples
This filter injects Javascript code to implement Cross Site Request Forgery.
<filter>
<filter-name>CSRFInjectionFilter</filter-name>
<filter-class>ch::nevis::isiweb4::filter::validation::CSRFFilter</filter-class>
<init-param>
<param-name>InjectionScriptPath</param-name>
<param-value>/opt/nevisproxy/template/conf/csrf-inject.js</param-value>
</init-param>
<init-param>
<param-name>InjectedIdCheck</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>IdQueryParamName</param-name>
<param-value>csrfpId</param-value>
</init-param>
<init-param>
<param-name>ProtectedURIs</param-name>
<param-value>/foo/.*</param-value>
</init-param>
</filter>
Using Content Security Policies with the CSRFFilter
The following CSRFFilter loads the injection script externally which is needed when Content Security Policies are set.
<filter>
<filter-name>CSRFInjectionFilter</filter-name>
<filter-class>ch::nevis::isiweb4::filter::validation::CSRFFilter</filter-class>
<init-param>
<param-name>InjectionScriptPath</param-name>
<param-value>/js/csrf-inject-ext.js</param-value>
</init-param>
<init-param>
<param-name>IncludeInjectionScriptExternally</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>InjectedIdCheck</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>IdQueryParamName</param-name>
<param-value>csrfpId</param-value>
</init-param>
<init-param>
<param-name>ProtectedURIs</param-name>
<param-value>/foo/.*</param-value>
</init-param>
</filter>
A complete configuration is available in CSRFfilter_with_CSP.example
.
Modifying the injected JavaScript to rewrite only specific links
If there are links pointing to non-protected URIs, you can modify the injected JavaScript to avoid these unprotected-URI links, and to rewrite only specific links. Below follows an example of how to do this.
First, replace the csrfCheckDomain function with the following one:
function csrfCheckDomain(attribute) {
var index;
index = attribute.indexOf("//");
if(index != 0 && index != 5 && index != 6) {
return relativeCheck(attribute)
}
index = index + 2;
try {
var desthost = attribute.substring(index, location.hostname.length+index);
var destpath = attribute.substring(location.hostname.length+index);
}
catch(e) {
return false;
}
if (desthost == location.hostname) {
return relativeCheck(destpath);
}
return false;
}Then add one of the next relativeCheck function implementations:
Rewrite links pointing to a specific path:
function relativeCheck(path) {
var protected = "/example/auth/emailchange";
return (path.indexOf(protected) == 0)
}Rewrite links pointing to the same page:
function relativeCheck(path) {
return (path.indexOf(location.pathname) == 0)
}
Handling very simple HTML documents
The CSRF filter requires a minimal structure of the HTML page. The page must include at least an html, head, and body tag. Although normal documents always have these tags, the HTML standard allows for missing these tags. Even a plaintext document is valid HTML.
The CSRF filter can handle missing head or body tags. But if your document lacks all of the abovementioned tags, you need to add them with a LuaFilter. Map the LuaFilter between the backend and the CSRFFilter. For an example, see the file LuaFilter_simple_html_fixer_for_CSRF.example.