Extended Access Control module for Apache

# mod_eaccess: Extended Access control

This module enables Regular Expression filtering on URL, including HTTP method, URI, QUERY_STRING and body content (DATA).

## Table of Contents

- Download
- Summary
- Installation
- Directives
- Limitation
- Security tips

## Download

03/2001: current version is 2.3.3.9.

## Summary

This module is specially designed for Apache server to act as a secure reverse proxy server, filtering access to CGI and their parameters.
Here is an example:

    # Deny all
    EAccessEnable on

    # Log grants and denies to logs/eaccess_log
    EAccessLogLevel 1

    # Allow cgi toto, called by GET, without any QUERY_STRING.
    EAccessRule permit "^GET /cgi-bin/toto$"

    # Allow cgi titi, called by GET, with a QUERY_STRING starting with the text
    # "field1=".
    EAccessRule permit "^GET /cgi-bin/titi\?field1="

    # The same, called by POST.
    EAccessRule permit "^POST /cgi-bin/titi\|field1="

    # Allow cgi tata, called by GET, with 2 arguments. Each argument contains
    # up to 32 characters.
    EAccessRule permit "^GET /cgi-bin/tata\?field1=[^&|]{0,32}&field2=[^&|]{0,32}$"

    # Deny all others cgi
    EAccessRule deny "^GET /cgi-bin/"
    EAccessRule deny "^GET /.*\.cgi"

    # Allow all others URL, called by GET
    EAccessRule permit "^GET /"

## Installation

Extract apache:

`% tar zxf apache_1.x.y.tar.gz`

Extract mod_eaccess:

`% tar zxf mod_eaccess-x.y.tar.gz`

Configure apache with mod_eaccess:

`% cd apache_1.x.y % ./configure --add-module=../mod_eaccess-x.y/mod_eaccess.c ...`

Compile:

`% make`

See Limitation if you want special installation (specialy for complete body checks).

## Directives

- EAccessEnable
- EAccessRule
- EAccessLog
- EAccessLogLevel
- EAccessCache
- EAccessOptim
- EAccessTmpDir

### EAccessEnable

Syntax: EAccessEnable `on` | `off`
Default: `EAccessEnable on`
Context: server config, virtual host

The EAccessEnable directive enables or disables the runtime extended access control engine.

If it is set to `off` this module does no runtime processing at all.
If it is set to `on` this module does runtime processing and then first sets default policy to deny all.

### EAccessRule

Syntax: EAccessRule action "pattern" [option]
Default: none
Context: server config, virtual host

The `EAccessRule` directive is the real extended access control workhorse. The directive can occur more than once. Each directive then defines one single access control rule. The definition order of these rules is important, because this order is used when applying the rules at run-time.

pattern can be extended regular expression which gets applied to the current URL. The following "options" can be used for pattern:

- `"!pattern"`
  Obsolete synonym for `-v`;

- `"-option:pattern"`
  Apply the following option> to the pattern:
  - -i
    Ignore case distinctions in pattern;
  - -v
    Invert the sense of matching, to select non-matching pattern;

Option can be combined; for example, you can use: `"-iv:pattern"`. If pattern must begin with `!` or with `-`, then you need to use a \escape.

`"Pattern"` is an Apache directive argument, so if pattern includes the `"` character, then you also need to use a \escape.

action can be one of `permit`, `deny`, `warning`, `auth/basic`, `auth/securid` or `exec`:

- `permit` "pattern"
  will be used to grant access to an URL matching pattern;

- `deny`[`=`err-code] "pattern"
  will be used to deny access to an URL matching pattern. An optional value, err-code, can be added to set the HTTP response code. If not set, default is FORBIDDEN;

- `warning` "pattern"
  will be used to log a warning for an URL matching pattern;

- `auth/basic`[`=`TTL] "pattern" ["realm"]
  will be used to check the HTTP/Basic authentication header for an URL matching pattern. A TTL (seconds), can be added to set the Time To Live of the authentication. If not set, default TTL is 0, which means no timeout. When the authentication is expired (due to TTL), the optional realm, if set, will enable the module to return an HTTP authentication request (error 401);

- `auth/securid`[`=`TTL] "pattern" ["redirect"]
  will be used to check the SecurID authentication header for an URL matching pattern. A TTL (seconds), can be added to set the Time To Live of the authentication. If not set, default TTL is 0, which means no timeout. When the authentication is expired (due to TTL), the optional redirect, if set, will enable the module to redirect the browser (error 302);

- `exec`[`=`err-code] "pattern" "command"
  will be used to grant or deny access to an URL matching pattern due to command results. command is a shell command: any valid shell syntax can be used but the first word of command, for security raison, must have an absolute pathname. If command prints something to stderr, then access will be denied, else granted.
  If you want command to access body content, then use "`%s`" in "command" string. If you are using mod_eaccess with `BODY_HACK` (see Limitation), then "`%s`" will be converted to the body filename; command can read this file to access body content. If you are not using `BODY_HACK`, then "`%s`" will be converted to "`-`"; command can then read its stdin to access body content.
  If you use "`%s`" and the URL has no body, then it will be converted to the keyword `none`.

For each requested URL, the module constructs the following string for controls:

- if arguments are available, they are added to method and URI with a `?`:
  "METHOD URI?QUERY_STRING",
- if body is available, its content is added to method and URI with a `|`:
  "METHOD URI|BODY",

Note that both QUERY_STRING and body may be present, so the string is:

- "METHOD URI[?QUERY_STRING][|BODY]"

Then this string is unescaped: `%encoded' characters are translated into their corresponding values, except for: `
%00 ('\0') => "\0",
%07 ('\a') => "\a",
%08 ('\b') => "\b",
%0A ('\n') => "\n",
%0B ('\v') => "\v",
%0C ('\f') => "\f",
%0D ('\r') => "\r",
%26 ('&') => ".",
%7C ('|') => ".".`
`CR-LF` (character `\r` followed by character `\r`) are also translated into string `\n`.

This makes URL arguments and body easier to match.

Then the unescaped string is used by the module to try to match a pattern defined in `EAccessRule`.

As default policy is set to deny all when extended access control is set to on, the algorithm used for each URL by the module is:

- deny all,
- for each pattern defined with `EAccessRule` directive:
  - if pattern matches URL (does not match for `!`pattern), then:
    - for `permit` action: access is granted and loop stops,
    - for `deny` action: access is denied (error 403 or err-code) and loop stops,
    - for `warning` action: a `** WARNING **` is logged,
    - for `auth/*` action:
      - if there is no auth in HTTP header:
        - if option is not set, loop continues,
        - else (option is set):
          - for `auth/basic`: access is denied (error 401 with the realm is returned) and loop stops,
          - for `auth/securid`: access is redirected (error 302 with the the redirect is returned) and loop stops,
      - else (there is an auth in HTTP header):
        - if this is the first time this auth is used, time is stored, and loop continues,
        - else (this is not the first time this auth is used):
          - if auth is not expired (due to TTL), loop continues,
          - else (auth is expired):
            - if option is not set, auth is discarded in the HTTP header and loop continues,
            - else (option is set):
              - for `auth/basic`: access is denied (error 401 with the realm is returned) and loop stops,
              - for `auth/securid`: access is redirected (error 302 with the redirect is returned) and loop stops,
    - for `exec` action:
      - if command prints nothing to stderr, access is granted,
      - else (something on stderr), access is denied (error 403 or error-code),

In fact, for `auth/*` action, if option is set in the rule, we do not trust the web server because authentication is first checked by mod_eaccess.
If option is not set, we do trust the web server and then do not check if an authentication is set in the HTTP header.

### EAccessLog

Syntax: EAccessLog file-pipe
Default: `EAccessLog logs/eaccess_log`
Context: server config, virtual host

The `EAccessLog` directive sets the name of the file to which the server logs any extended access controls it performs. If the name begins with a pipe ('|'), followed by a command, then it is assumed to be a program that receives the agent log information on its standard input. Else, it must be a filename. If the filename does not begin with a slash ('/') then it is assumed to be relative to the Server Root.

When `EAccessLogLevel` is set, each action logs a line in the common log format (host, ident, authuser, date), followed by a text, depending upon the action:

- permit: when RE #nnn (first RE is #1) matches a pattern:

  - `RE #nnn grants access to '`METHOD URI[?args[|data]]`'`

- deny: when RE #nnn matches a pattern, or when no rule matches:

  - `RE #nnn denies access to '`METHOD URI[?args[|data]]`'`
  or
  - `default denies access to '`METHOD URI[?args[|data]]`'`

- warning: when RE #nnn matches a pattern:

  - `RE #nnn *** WARNING! *** '`METHOD URI[?args[|data]]`'`

- auth/*:
  - `RE #nnn AUTH not needed '`METHOD URI[?args[|data]]`'`
    when no option is set in `EAccessRule auth/*`, and no auth HTTP header is present,

  - `RE #nnn AUTH starting on '`METHOD URI[?args[|data]]`'`
    when auth HTTP header is present and this is the first time this authentication is used,

  - `RE #nnn AUTH unTTLed for '`METHOD URI[?args[|data]]`'`
    when auth HTTP header is present and not expired because no TTL is used,

  - `RE #nnn AUTH not expired '`METHOD URI[?args[|data]]`'`
    when auth HTTP header is present and not yet expired,

  - `RE #nnn AUTH too old for '`METHOD URI[?args[|data]]`'`
    when option is set in `EAccessRule auth/*`, and auth HTTP header is expired (error 401 for auth/basic or 302 for auth/securid is returned),

  - `RE #nnn AUTH removed for '`METHOD URI[?args[|data]]`'`
    when no option is set in `EAccessRule auth/*`, and auth HTTP header is expired (this header is then removed),

  - `RE #nnn AUTH err 401 for '`METHOD URI[?args[|data]]`'`
    when a realm is set in `EAccessRule auth/basic`, and no `Authenticate:` HTTP header is present,

  - `RE #nnn AUTH err 302 for '`METHOD URI[?args[|data]]`'`
    when a redirect is set in `EAccessRule auth/securid`, and no `Cookie: AceHandle` HTTP header is present,

- exec: when RE #nnn matches a pattern:
  - `RE #nnn grants access to '`METHOD URI[?args[|`<datas...>`]]`'` [`because '`stdout...`'`]
    when command succeded. Only the begenning of stdout..., if available, is used. Also notice that the special keyword `<datas...>` is used instead of body content;

  - `RE #nnn denies access to '`METHOD URI[?args[|`<datas...>`]]`' because '`stderr...`'`
    when command failed. Only the begenning of stderr... is used;

Notice: To disable the logging, it is not recommended to set filename to `/dev/null`, because although the module does not create output to a logfile it still creates the logfile output internally. This will slow down the server! To disable logging use `EAccessLogLevel 0`.

### EAccessLogLevel

Syntax: EAccessLogLevel level
Default: `EAccessLogLevel 1`
Context: server config, virtual host

The `EAccessLogLevel` directive set the verbosity level of the extended access control logfile.

Level 0 means no logging.
Level 1 logs which `EAccessRule` grants or denies access to URL.
Level > 1 is for debugging.

### EAccessCache

Syntax: EAccessCache filename
Default: `EAccessCache logs/eaccess_auth`
Context: server config, virtual host

The `EAccessCache` directive sets the name of the file to which the server caches user authentication when auth/* is used in `EAccessRule`. If the name does not begin with a slash ('/') then it is assumed to be relative to the Server Root.

Notice: To avoid dump of the cache, MD5 digest of the authentications is stored...

### EAccessOptim

Syntax: EAccessOptim level
Default: `EAccessOptim 1`
Context: server config, virtual host

The `EAccessOptim` directive sets the optimization level.

Level `0` (default value) means no optimization: only regular expressions strings are stored in memory.
Level `1` means more optimization: compiled regular expressions are also stored in memory. With a bad regex lib, this may cost a lot... Use GNU regex.

### EAccessTmpDir

Syntax: EAccessTmpDir directory
Default: `EAccessTmpDir /tmp`
Context: server config, virtual host

The `EAccessTmpDir` directive sets the temporary directory used to store body files.

This directive is only available if you compile mod_eaccess with `-DEACCESS_BODY_HACK` (See Limitation).

## Limitation

Only the beginning of the body can be checked; limit is near DEFAULT_BUFSIZE (defined in buff.c), but also depends on system settings:

When Apache receives a connection, it is read from a socket and put into a buffer. Default socket buffer size is controlled with:

- `net.core.rmem_default` for Linux:
  - `sysctl net.core.rmem_default` to get current limit;
  - `sysctl -w net.core.rmem_default=nnn` to set limit;

- `tcp_recv_hiwat` for Solaris:
  - `ndd /dev/tcp tcp_recv_hiwat` to get current limit;
  - `ndd -set /dev/tcp tcp_recv_hiwat nnn` to set limit;

So the first socket read of an incomming connection (limit: default socket buffer size, as Apache does not set it) is put into buffer (limit: DEFAULT_BUFSIZE, set to 4096 in buff.c). It is then a good idea to have default socket buffer size greater than DEFAULT_BUFSIZE: we recommend to use at least 8192.

Finally, as the beginning of the HTTP connection includes headers and body, EAccess will use at most for body: DEFAULT_BUFSIZE - sizeof (HTTP headers).

To bypass this limit, we must patch Apache code. The raison is simple: when Apache has read once the body, it cannot read it again (because of socket...). The little patch `mod_eaccess.patch` is supplied to enable complete body checks. Do the following:

Patch apache:

`% cd apache_1.x.y % patch -b -p 0 < ../mod_eaccess-x.y/mod_eaccess.patch`

Compile apache with complete body future:

`% cd apache_1.x.y % make EXTRA_CFLAGS=-DEACCESS_BODY_HACK`

## Security tips

As we said, the string used to match a RE is:

"METHOD URI [ ?QUERY_STRING ] [ |BODY ]"

In fact, `URI` is set by Apache as the path portion of the %unescaped unparsed uri, with some path optimisations. For example:

Unparsed URI | URI | because
`/some/path?some+args` | `/some/path` |
`/some/p%61th` | `/some/path` | 0x61 is 'a'
`/some/../path` | `/path` | .. optimization
`/good%3Ffoo/../bad` | `/bad` | 0x3F is '?', but path optim. occurs before %unescape
`/good%3Ffoo/%2E./bad` | `/good?foo/../bad` | %2E is '.', but no path optim., just %unescape

And the string used to match a RE is exactly:

"unescape (METHOD escape (URI) [ ?QUERY_STRING ] [ |BODY ]"

When EAccess is used with Apache running as a web-server, this is fine because this string is really the URI Apache is looking for.

But when EAccess is used with Apache running as a reverse-proxy, this may be dangerous; suppose for example the URL the reverse-proxy receives is:

`"GET /good.cgi%3Fparam=/%2E./bad.cgi"`

The URI in the reverse-proxy running with EAccess will be:

`"/good.cgi?param=/../bad.cgi"` (%unescape URI)

and will be proxy as:

`"GET /good.cgi%3Fparam=/../bad.cgi"` (%escape URI)

Then the next server (suppose Apache), will compute his URI as:

`"/bad.cgi"` (path optimization)

This may be very dangerous if EAccess rules are:

`permit "^GET /good\.cgi\?param=.{1,64}$"`

This allows `good.cgi` to be used with a argument `param` which contains up to 64 characters. For example:

`GET /good.cgi?param=foobar"`

But this also allows the following URL:

`GET /good.cgi%3Fparam=/%2E./bad.cgi?badargs"`

which is scanned by EAccess filter as `"/good.cgi?param=/../bad.cgi?badargs"`, resend by reverse-proxy as `"/good.cgi%3Fparam=/../bad.cgi?badargs"` and parsed by next server as `"/bad.cgi?badargs"`.

Conclusion: when using EAccess on Apache running as a reverse-proxy, it is a good idea to use the first rule:

`deny "%[a-fA-F0-9]{2}"`

### Apache HTTP Server Version 1.3