YARP ("Yet Another Reverse Proxy") is a reverse proxy toolkit for ASP.NET Core. This project extends YARP's functionality by adding firewall capabilities.
Being an extension to YARP, this project follows much of the conventions in the YARP project, both in terms of solution and class structure. This also means that it can be configured in the same way as YARP; it supports configuration files, as well as a configuration API for programmatic, in-process configuration.
This project is currently in an early stage (
At present, Yarp.Extensions.Firewall contains just a custom rule engine. The custom rule engine is heavily influenced by Azure WAF (found as part of Application Gateway and Front Door).
Firewalls with custom rules are configured per route (matching on the RouteId configured in YARP). Each route's firewall has a few basic settings
RouteId- the name of the route, as configured in YARPEnabled- Enable (or disable)Mode- how the firewall should operate;Detection(log only), orPrevention(enforce rules)BlockedStatusCode- HTTP Status Code to return when a request is deniedRedirectUri- URL to return when a request is redirectedRules- the set of custom rules for this
Custom Rules are configured as sets of conditions, and are executed according to their given priority; all conditions in a rule must match the request for the rule to be enforced with the specified action (ie. there is an implicit AND between all conditions in a rule). If all of a rule's conditions match the request, no other rules are evaluated. If no rules match, the request is implicitly allowed to continue to the YARP middleware.
RuleName- the name or description given to the rulePriority- a number indicating what priority the rule should be given.0is highest priorityAction- the action that should be taken when all conditionsAllow- the request is explicitly allowed to continueBlock- the request is denied and cannot continue; the firewall'sBlockedStatusCodeis returned to the clientLog- the request is logged, and allowed to continueRedirect- a Redirect response is returned to the client, with the location set to the firewall'sRedirectUri
Conditions- the set of conditions that define this rule
A number of conditions are supported, with different configuration options depending on the property of the request being evaluated. All conditions have the below options
MatchType- the type of value that will be evaluatedSize- the length of a request property will be evaluatedString- evaluation depends on a particular value in one of the request's propertiesIPAddress- match on a given list or range of IP addressesGeoIP- match on a given country as determined by MaxMind GeoIP2 from the IP address
Negate- the evaluation result will be inverted; ie. if a match was not found, the condition will returntrue(and vice versa)
When evaluating an IPAddress MatchType, either the client's socket address or the perceived remote address can be used for the match. That value will be evaluated against the given IP address, list of IP addresses, or CIDR range.
MatchVariable- the property to retrieve the request's IP address fromSocketAddress- use the IP address from the actual connection; if the request was previously proxied, this might not be the actual client's address but rather the address of the proxyRemoteAddress- the perceived client's address; at present, this will be the first valid IP address from theForwardedandX-Forwarded-Forheaders, plus the socket address
IPAddressOrRanges- the IP address(es) to evaluate against, and accepts both IPv4 and IPv6 addresses. This may be either a single IP address, a comma-separated list of IP address, or a CIDR range
When MatchType is Size, evaluation is performed by comparing the configured MatchValue to the length of the configured request property. Some request properties require an additional Selector that specifies an additional key. A series of transformations can be done on the value prior to the evaluation itself as well.
MatchVariable- the request property to be evaluated. Valid values areRequestMethod- the HTTP Method for the request (GET,POST,HEADetc)QueryParam- evaluate the length of the query parameter given bySelectorPostArgs- evaluate the length of the HTTP Form POST parameter given bySelectorRequestPath- evaluates the length of the relative URI, including the entire query stringRequestHeader- evaluate the length of the particular request header given bySelectorRequestBody- evaluate the length of the entire request bodyCookie- evaluate the size of the particular request cookie given bySelector
Operator- the type of comparison to use againstMatchValueafter transformations are appliedLessThanGreaterThanLessThanOrEqualGreaterThanOrEqual
Selector- a key indicating whichCookie,RequestHeader,PostArgs, orQueryParamvalue to use, if it existed in the requestMatchValue- the value to be compared againstTransforms- a list of transformations to be applied, in order
ASP.NET Core and Kestrel have their own limits on request sizes, and in general these should be preferred over general/global RequestBody size rules.
Keep in mind that those limits will apply even if the firewall is in Detection mode, as they are inherent to the underlying server itself.
A MatchType of String will evaluate request properties against a list of values to determine a match. Like the Size evaluators, an additional key specified by Selector is required for some request properties, and transformations can be applied before the value is evaluated.
MatchVariable- the request property to be evaluated. Valid values areRequestMethod- the HTTP Method for the request (GET,POST,HEADetc)QueryParam- the query parameter given bySelectorPostArgs- the HTTP Form POST parameter given bySelectorRequestPath- the relative URI, including the entire query stringRequestHeader- the particular request header given bySelectorRequestBody- the entire request bodyCookie- the particular request cookie given bySelector
Operator- the type of case-sensitive string comparison to use for evaluation after transformations are appliedAny- the property contains any valueEquals- the property exactly equals one of theMatchValuesContains- the property contains any of theMatchValuesStartsWith- the property starts with one of theMatchValuesEndsWith- the property ends with one of theMatchValuesRegex- the property matches one of the regular expression patterns given inMatchValues
Selector- a key indicating whichCookie,RequestHeader,PostArgs, orQueryParamvalue to use, if it existed in the requestMatchValues- a list of string values to be compared againstTransforms- a list of transformations to be applied, in order
The GeoIP value for MatchType will look up the client's country based on the IP address from either the socket address or remote address, and evaluate this against a list of supplied country names.
MatchVariable- the property to retrieve the request's IP address fromSocketAddress- use the IP address from the actual connection; if the request was previously proxied, this might not be the actual client's address but rather the address of the proxyRemoteAddress- the perceived client's address; at present, this will be the first valid value from theX-Forwarded-Forheader, falling back to the socket address if none was found
MatchCountryValues- a list of country names (not case sensitive) to be evaluated against
The Yarp.Extensions.Firewall.MaxMindGeoIP library (in this solution) allows the use of a MaxMind GeoIP2 Country database for this purpose.
This package must be referenced in your project, and added to the service collection via IFirewallBuilder.AddMaxMindGeoIP().
The path to a GeoIP2 or GeoLite2 Country database is configured by a GeoIPDatabaseConfig object inside the firewall configuration (adjacent to RouteFirewalls), containing a GeoIPDatabasePath property. As with all other configuration values, the database path can be updated without requiring a restart, and as MaxMind frequently updates the databases (at time of writing, twice weekly) frequent updating is encouraged.
No database files are provided in this project, however one to suit your purpose (commercial, enterprise, or free) can be obtained from MaxMind. Note you will need the Country database, and supplying any other type will fail to load the database and any configured GeoIP evaluators.
Alternatively, other GeoIP databases can be used by implementing the IGeoIPDatabaseProviderFactory and IGeoIPDatabaseProvider interfaces, and registering them with the service collection with IFirewallBuilder.Services.TryAddSingleton<IGeoIPDatabaseProviderFactory, YourGeoIPDatabaseProviderFactory>(). Configuration for your implementation can be done by implementing the IFirewallConfigurationExtensionProvider interface (or extending the FirewallConfigurationExtensionProvider class), and registering it with the service collection with IFirewallBuilder.AddConfigurationExtensionProvider<YourFirewallConfigurationExtensionProvider>().
(This also works for any other way you would like to extend the firewall functionality.)
Transformations can be applied to the request values for Size and String evaluators prior to any comparisons to do things like changing the case, trimming whitespace, or applying URL decoding/encoding. Transforms are applied in the order given in the condition configuration.
Uppercase- convert the request value to upper-caseLowercase- convert the request value to lower-caseTrim- remove any whitespace characters from the start and end of the valueUrlDecode- convert any URL-encoded characters. This also accounts for repeat encodings, a common bypass techniqueUrlEncode- convert any special characters to their URL-encoded representation
(Case transformations don't affect Size evaluations, and are automatically ignored in that case.)
Below is an example of what this configuration looks like (as used in ConfigurationConfigProviderTests).
The parent section to "RouteFirewalls" must be passed to the .LoadFromConfig() extension method.
For example, place "RouteFirewalls" inside the section used for YARP (eg. "ReverseProxy"), alongside the "Routes" and "Clusters".
{
// ...
"ReverseProxy": {
"Routes": {
"routeA": { ... },
"routeB": { ... }
},
"Clusters": { ... }
"RouteFirewalls": {
"routeA": {
"Enabled": true,
"Mode": "Prevention",
"RedirectUri": "https://localhost:10000/blocked",
"BlockedStatusCode": "Forbidden",
"Rules": {
"stringAndSize": {
"Priority": 10,
"Action": "Block",
"Conditions": [
{
"MatchType": "String",
"Operator": "Contains",
"MatchVariable": "QueryParam",
"Selector": "a",
"MatchValues": [ "1" ],
"Transforms": [
"Trim",
"UrlDecode",
"Uppercase"
]
},
{
"MatchType": "Size",
"Operator": "GreaterThanOrEqual",
"MatchVariable": "Cookie",
"Selector": "b",
"MatchValue": 100
}
]
},
"ipAddress1": {
"Priority": 11,
"Action": "Allow",
"Conditions": [
{
"MatchType": "IPAddress",
"IPAddressOrRanges": "2001::abcd",
"MatchVariable": "SocketAddress"
}
]
}
}
},
"routeB": {
"Enabled": true,
"Mode": "Detection",
"RedirectUri": "https://localhost:20000/blocked.html",
"BlockedStatusCode": "NotFound",
"Rules": {
"ipAddress2": {
"Priority": 5,
"Action": "Allow",
"Conditions": [
{
"MatchType": "IPAddress",
"IPAddressOrRanges": "192.168.0.0/16",
"MatchVariable": "RemoteAddress"
}
]
}
}
}
},
"GeoIPDatabaseConfig": {
"GeoIPDatabasePath": "./path/to/GeoLite2-Country.mmdb"
}
}
}I'm eager to accept contributions and suggestions in any form. Please feel free to open an issue, discussion, or PR, or to message me on here or Mastodon.