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.
apache_error.log:
[Sun Oct 23 12:33:08 2016] [error] [client 127.0.0.1] (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];
HANDLE hFind;
if ((rv = test_safe_name(fname)) != APR_SUCCESS) {
return APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND); // was: rv;
}
hFind = FindFirstFileW(wfname, &FileInfo.w);
if (hFind == INVALID_HANDLE_VALUE)
return APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND); // was: apr_get_os_error();
FindClose(hFind);
if (unicode_to_utf8_path(tmpname, sizeof(tmpname), FileInfo.w.cFileName)) {
return APR_ENAMETOOLONG;
}
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;
else
return (*name == ':') ? APR_FROM_OS_ERROR(ERROR_FILE_NOT_FOUND) : APR_EBADPATH; // was: APR_EBADPATH;
}
++name;
}
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
.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
.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
.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
.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
.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
.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: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
.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
.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: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.