I develop this site with MAMP Apache under Windows and GRAV. I get 'Forbidden' errors on the development machine with links like http://localhost/page:2, which are used a lot in GRAV. Links like http://localhost/blog/page:2 work fine. And everything works fine on my 'production' server with Apache on Ubuntu, which is not consistent and makes development difficult.


[Sun Oct 23 12:33:08 2016] [error] [client] (20024)The given path is misformatted or contained invalid characters: Cannot map GET /page:2 HTTP/1.1 to file, referer: http://localhost/

I tracked this issue down: Apache will refuse to serve a REQUEST URI with colon on Windows. The workaround is to go one level deeper, but it is not a general solution for consistent behaviour of your trusty webserver. The StackOverflow discussions are fragmented here, here and here. The Apache bug report is marked as RESOLVED WONTFIX. The reason: possible safety issues with NTFS streams, where colon is used as separator. The reasoning, incl. congratulations does not really convince me, as it breaks development with many good tools like GRAV or MediaWiki on Windows.

William A. Rowe Jr. wrote on 2009-12-01:

Folks, this won't be addressed until httpd learns the concept of "not a file" resource, al la contextual DocumentRoot per Location/VirtualHost. E.g. a proxy-only namespace, or something run exclusively through a special handler.

We got 7 years older, this did not happen yet, but people keep falling into this old trap.

At first I thought modifing apr_c_is_fnchar to allow colon would help, but it just brought 'Forbidden' with Windows error 123, i.e. ERROR_INVALID_NAME.
Hui Jin proposed this patch to apr_stat to return ERROR_FILE_NOT_FOUND for any error in test_safe_name and FindFirstFileW:

/* Guard against bogus wildcards and retrieve by name
 * since we want the true name, and set aside a long
 * enough string to handle the longest file name.
char tmpname[APR_FILE_MAX * 3 + 1];
if ((rv = test_safe_name(fname)) != APR_SUCCESS) {
    return APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND); // was: rv;
hFind = FindFirstFileW(wfname, &FileInfo.w);
    return APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND); // was: apr_get_os_error();
if (unicode_to_utf8_path(tmpname, sizeof(tmpname), FileInfo.w.cFileName)) {
filename = apr_pstrdup(pool, tmpname);

Seems to be a crude patch. I am not sure about the second change, but the first one makes the difference. The mapped ERROR_FILE_NOT_FOUND seems to be propagated as 'not a file, but let us process it'. But it means you will not get 'Forbidden' error for any invalid characters, which is dangerous and not consistent with production.

A much better patch would be for test_safe_name() in srclib\apr\file_io\win32\filestat.c to return ERROR_FILE_NOT_FOUND only for names with colon. We could also make use of free bits of apr_c_is_fnchar, if there are more cases like this. I think we are on the safe side, as Apache is not going to touch the file.

/* We have to assure that the file name contains no '*'s, or other
 * wildcards when using FindFirstFile to recover the true file name.
static apr_status_t test_safe_name(const char *name)
    /* Only accept ':' in the second position of the filename,
     * as the drive letter delimiter:
    if (apr_isalpha(*name) && (name[1] == ':')) {
        name += 2;
    while (*name) {
        if (!IS_FNCHAR(*name) && (*name != '\\') && (*name != '/')) {
            if (*name == '?' || *name == '*')
                return APR_EPATHWILD;
                return (*name == ':') ? APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND) : APR_EBADPATH; // was: APR_EBADPATH;
    return APR_SUCCESS;

I tried to avoid recompiling Apache by patching the libapr-1.dll directly. I fired up IDA PRO and looked into the code. Unfortunately the debug symbols are not available, but I found apr_stat export and followed to test_safe_name to see this snippet of assembly:

.text:6EEC47B0                   test_safe_name  proc near               ; CODE XREF: apr_stat(x,x,x,x)+101p
.text:6EEC47B0 56                                push    esi
.text:6EEC47B1 8B F0                             mov     esi, eax
.text:6EEC47B3 0F B6 06                          movzx   eax, byte ptr [esi]
.text:6EEC47B6 50                                push    eax             ; int
.text:6EEC47B7 FF 15 30 B3 ED 6E                 call    ds:isalpha
.text:6EEC47BD 83 C4 04                          add     esp, 4
.text:6EEC47C0 85 C0                             test    eax, eax
.text:6EEC47C2 74 09                             jz      short @@not_ch_colon
.text:6EEC47C4 80 7E 01 3A                       cmp     byte ptr [esi+1], ':'
.text:6EEC47C8 75 03                             jnz     short @@not_ch_colon
.text:6EEC47CA 83 C6 02                          add     esi, 2
.text:6EEC47CD                   @@not_ch_colon:                         ; CODE XREF: test_safe_name+12j
.text:6EEC47CD                                                           ; test_safe_name+18j
.text:6EEC47CD 8A 06                             mov     al, [esi]
.text:6EEC47CF 84 C0                             test    al, al
.text:6EEC47D1 74 29                             jz      short @@leave0
.text:6EEC47D3 B9 01 00 00 00                    mov     ecx, 1
.text:6EEC47D8 EB 06                             jmp     short @@next
.text:6EEC47D8                   ; ---------------------------------------------------------------------------
.text:6EEC47DA 8D 9B 00 00 00 00                 align 10h
.text:6EEC47E0                   @@next:                                 ; CODE XREF: test_safe_name+28j
.text:6EEC47E0                                                           ; test_safe_name+4Aj
.text:6EEC47E0 0F B6 D0                          movzx   edx, al
.text:6EEC47E3 84 8A D0 B7 ED 6E                 test    ds:apr_c_is_fnchar[edx], cl
.text:6EEC47E9 75 08                             jnz     short @@char_ok
.text:6EEC47EB 3C 5C                             cmp     al, '\'
.text:6EEC47ED 74 04                             jz      short @@char_ok
.text:6EEC47EF 3C 2F                             cmp     al, '/'
.text:6EEC47F1 75 0D                             jnz     short @@char_nok
.text:6EEC47F3                   @@char_ok:                              ; CODE XREF: test_safe_name+39j
.text:6EEC47F3                                                           ; test_safe_name+3Dj
.text:6EEC47F3 8A 04 0E                          mov     al, [esi+ecx]
.text:6EEC47F6 03 F1                             add     esi, ecx
.text:6EEC47F8 84 C0                             test    al, al
.text:6EEC47FA 75 E4                             jnz     short @@next
.text:6EEC47FC                   @@leave0:                               ; CODE XREF: test_safe_name+21j
.text:6EEC47FC 33 C0                             xor     eax, eax
.text:6EEC47FE 5E                                pop     esi
.text:6EEC47FF C3                                retn
.text:6EEC4800                   ; ---------------------------------------------------------------------------
.text:6EEC4800                   @@char_nok:                             ; CODE XREF: test_safe_name+41j
.text:6EEC4800 8A 06                             mov     al, [esi]
.text:6EEC4802 3C 3F                             cmp     al, '?'
.text:6EEC4804 74 0B                             jz      short @@not_wildcard ; APR_EBADPATH
.text:6EEC4806 3C 2A                             cmp     al, '*'
.text:6EEC4808 74 07                             jz      short @@not_wildcard ; APR_EBADPATH
.text:6EEC480A B8 38 4E 00 00                    mov     eax, 4E38h      ; APR_EPATHWILD
.text:6EEC480F 5E                                pop     esi
.text:6EEC4810 C3                                retn
.text:6EEC4811                   ; ---------------------------------------------------------------------------
.text:6EEC4811                   @@not_wildcard:                         ; CODE XREF: test_safe_name+54j
.text:6EEC4811                                                           ; test_safe_name+58j
.text:6EEC4811 B8 39 4E 00 00                    mov     eax, 4E39h      ; APR_EBADPATH
.text:6EEC4816 5E                                pop     esi
.text:6EEC4817 C3                                retn
.text:6EEC4817                   test_safe_name  endp
.text:6EEC4817                   ; ---------------------------------------------------------------------------
.text:6EEC4818 CC CC CC CC CC CC CC CC           align 10h```

Fortunately, our case is at the end of the function and thanks to alignment there are 8 free bytes (close to the mean value - and 6 more above :). This is not much, as mov eax, constant32 takes 5 bytes (APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND = 2) = 0AFC82h), so I had to code tight. I came to this solution (Edit / Patch program / Assemble, then Apply patches):

.text:6EEC4800                   @@char_nok:                             ; CODE XREF: test_safe_name+41j
.text:6EEC4800 8A 06                             mov     al, [esi]
.text:6EEC4802 BB 38 4E 00 00                    mov     ebx, 4E38h      ; APR_EPATHWILD
.text:6EEC4807 3C 3F                             cmp     al, '?'
.text:6EEC4809 75 05                             jnz     short is_colon
.text:6EEC480B 3C 2A                             cmp     al, '*'
.text:6EEC480D 75 01                             jnz     short @@is_colon
.text:6EEC480F 43                                inc     ebx             ; APR_EBADPATH
.text:6EEC4810                   @@is_colon:                             ; CODE XREF: test_safe_name+59j
.text:6EEC4810                                                           ; test_safe_name+5Dj
.text:6EEC4810 3C 3A                             cmp     al, ':'
.text:6EEC4812 75 05                             jnz     short @@ret_ebx
.text:6EEC4814 BB 82 FC 0A 00                    mov     ebx, 0AFC82h    ; APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND)
.text:6EEC4819                   @@ret_ebx:                              ; CODE XREF: test_safe_name+62j
.text:6EEC4819 89 D8                             mov     eax, ebx
.text:6EEC481B 5E                                pop     esi
.text:6EEC481C C3                                retn
.text:6EEC481C                   test_safe_name  endp
.text:6EEC481C                   ; ---------------------------------------------------------------------------
.text:6EEC481D CC CC CC                          db 3 dup(0CCh)

It seems to work fine and there are still some bytes left... Not beautiful, but helpful.

The current version of MAMP is 3.2.2 with Apache 2.2.31 (phpinfo). This is a "Legacy Version", a bit dated (July 2015), so I will probably have to do it again one day... Perhaps I should propose an official patch after some testing. Anyhow here is the original and patched DLL. Back up the original c:\MAMP\bin\apache\bin\libapr-1.dll, unpack, compare and test.

Use this patch with care. It is meant for development, not tested for production use!

BTW I could not configure MAMP UI language, so I went for a crude solution described here.

Add a comment

Next Post Previous Post