To execute CGI scripts, a Web server must be able to access the interpreter used for that script. But what if you directly request site.com/cgi-bin/php.ini
or site.com/cgi-bin/php.cgi?index.php
? If either show up thats a major problem, try it on your site.
The solution is that when you request /index.php, Apache or whatever server you are using does a subrequest/internal request to the php interpreter at /cgi-bin/php.cgi
, and when it does an internal request like that it adds some special environment variables that are normal variables prefixed with a REDIRECT_
.
We only want internal/sub redirected requests to be allowed to access /cgi-bin/php.ini
and /cgi-bin/php.cgi
, and .htaccess provides several methods to achieve this type of access control.
By using the AddHandler and Action directives below, we are setting up Apache to automatically set the REDIRECT_STATUS (also PATH_TRANSLATED which is important for suEXEC among other things).
AddHandler php-cgi .php Action php-cgi /cgi-bin/php.cgi
Since we now know that we only want requests that have the REDIRECT_STATUS environment variable set, we can issue a 403 Forbidden to anything else. You can place this in your /cgi-bin/.htaccess file.
Order Deny,Allow Deny from All Allow from env=REDIRECT_STATUS
This can go in your /.htaccess file and uses regex to apply to php[0-9]\.(ini|cgi)
Order Deny,Allow Deny from All Allow from env=REDIRECT_STATUS
You may also use mod_rewrite's power to further tighten the access by only allowing for redirects with a 200 Status code. This could come into play if your default ErrorDocuments are themselves php scripts. An ErrorDocument 403 /error.php will have a REDIRECT_STATUS of 403.
ErrorDocument 403 /error.php RewriteEngine On RewriteBase / RewriteCond %{REQUEST_URI} ^.*\.(php|cgi)$ RewriteCond %{ENV:REDIRECT_STATUS} !200 RewriteRule .* - [F]
Using PHP as a CGI binary is an option for setups that for some reason do not wish to integrate PHP as a module into server software (like Apache), or will use PHP with different kinds of CGI wrappers to create safe chroot and setuid environments for scripts. This setup usually involves installing executable PHP binary to the web server cgi-bin directory.
Each new variable will have the prefix REDIRECT_. REDIRECT_ environment variables are created from the CGI environment variables which existed prior to the redirect, they are renamed with a REDIRECT_ prefix, i.e., HTTP_USER_AGENT becomes REDIRECT_HTTP_USER_AGENT. In addition to these new variables, Apache will define REDIRECT_URL and REDIRECT_STATUS to help the script trace its origin. Both the original URL and the URL being redirected to can be logged in the access log.
The suEXEC feature provides Apache users the ability to run CGI and SSI programs under user IDs different from the user ID of the calling web server. Normally, when a CGI or SSI program executes, it runs as the same user who is running the web server. Used properly, this feature can reduce considerably the security risks involved with allowing users to develop and run private CGI or SSI programs. However, if suEXEC is improperly configured, it can cause any number of problems and possibly create new holes in your computer's security. If you aren't familiar with managing setuid root programs and the security issues they present, we highly recommend that you not consider using suEXEC.
From suexec.c.
static const char *const safe_env_lst[] = { /* variable name starts with */ "HTTP_", "SSL_", /* variable name is */ "AUTH_TYPE=", "CONTENT_LENGTH=", "CONTENT_TYPE=", "DATE_GMT=", "DATE_LOCAL=", "DOCUMENT_NAME=", "DOCUMENT_PATH_INFO=", "DOCUMENT_ROOT=", "DOCUMENT_URI=", "GATEWAY_INTERFACE=", "HTTPS=", "LAST_MODIFIED=", "PATH_INFO=", "PATH_TRANSLATED=", "QUERY_STRING=", "QUERY_STRING_UNESCAPED=", "REMOTE_ADDR=", "REMOTE_HOST=", "REMOTE_IDENT=", "REMOTE_PORT=", "REMOTE_USER=", "REDIRECT_HANDLER=", "REDIRECT_QUERY_STRING=", "REDIRECT_REMOTE_USER=", "REDIRECT_STATUS=", "REDIRECT_URL=", "REQUEST_METHOD=", "REQUEST_URI=", "SCRIPT_FILENAME=", "SCRIPT_NAME=", "SCRIPT_URI=", "SCRIPT_URL=", "SERVER_ADMIN=", "SERVER_NAME=", "SERVER_ADDR=", "SERVER_PORT=", "SERVER_PROTOCOL=", "SERVER_SIGNATURE=", "SERVER_SOFTWARE=", "UNIQUE_ID=", "USER_NAME=", "TZ=", NULL };
Many sites that maintain a Web server support CGI programs. Often these programs are scripts that are run by general-purpose interpreters, such as /bin/sh or PERL. If the interpreters are located in the CGI bin directory along with the associated scripts, intruders can access the interpreters directly and arrange to execute arbitrary commands on the Web server system. All programs in the CGI bin directory can be executed with arbitrary arguments, so it is important to carefully design the programs to permit only the intended actions regardless of what arguments are used. This is difficult enough in general, but is a special problem for general-purpose interpreters since they are designed to execute arbitrary programs based on their arguments. *All* programs in the CGI bin directory must be evaluated carefully, even relatively limited programs such as gnu-tar and find.
If general-purpose interpreters are accessible in a Web server's CGI bin directory, then a remote user can execute any command the interpreters can execute on that server. The solution to this problem is to ensure that the CGI bin directory does not include any general-purpose interpreters, for example: PERL, Tcl, UNIX shells (sh, csh, ksh, etc.)
If you really want the details, start with modules/http/http_request.c of the apache source code.
AP_DECLARE(void) ap_die(int type, request_rec *r) { int error_index = ap_index_of_response(type); char *custom_response = ap_response_code_string(r, error_index); int recursive_error = 0; request_rec *r_1st_err = r; if (type == AP_FILTER_ERROR) { return; } if (type == DONE) { ap_finalize_request_protocol(r); return; } /* * The following takes care of Apache redirects to custom response URLs * Note that if we are already dealing with the response to some other * error condition, we just report on the original error, and give up on * any attempt to handle the other thing "intelligently"... */ if (r->status != HTTP_OK) { recursive_error = type; while (r_1st_err->prev && (r_1st_err->prev->status != HTTP_OK)) r_1st_err = r_1st_err->prev; /* Get back to original error */ if (r_1st_err != r) { /* The recursive error was caused by an ErrorDocument specifying * an internal redirect to a bad URI. ap_internal_redirect has * changed the filter chains to point to the ErrorDocument's * request_rec. Back out those changes so we can safely use the * original failing request_rec to send the canned error message. * * ap_send_error_response gets rid of existing resource filters * on the output side, so we can skip those. */ update_r_in_filters(r_1st_err->proto_output_filters, r, r_1st_err); update_r_in_filters(r_1st_err->input_filters, r, r_1st_err); } custom_response = NULL; /* Do NOT retry the custom thing! */ } r->status = type; /* * This test is done here so that none of the auth modules needs to know * about proxy authentication. They treat it like normal auth, and then * we tweak the status. */ if (HTTP_UNAUTHORIZED == r->status && PROXYREQ_PROXY == r->proxyreq) { r->status = HTTP_PROXY_AUTHENTICATION_REQUIRED; } /* If we don't want to keep the connection, make sure we mark that the * connection is not eligible for keepalive. If we want to keep the * connection, be sure that the request body (if any) has been read. */ if (ap_status_drops_connection(r->status)) { r->connection->keepalive = AP_CONN_CLOSE; } /* * Two types of custom redirects --- plain text, and URLs. Plain text has * a leading '"', so the URL code, here, is triggered on its absence */ if (custom_response && custom_response[0] != '"') { if (ap_is_url(custom_response)) { /* * The URL isn't local, so lets drop through the rest of this * apache code, and continue with the usual REDIRECT handler. * But note that the client will ultimately see the wrong * status... */ r->status = HTTP_MOVED_TEMPORARILY; apr_table_setn(r->headers_out, "Location", custom_response); } else if (custom_response[0] == '/') { const char *error_notes; r->no_local_copy = 1; /* Do NOT send HTTP_NOT_MODIFIED for * error documents! */ /* * This redirect needs to be a GET no matter what the original * method was. */ apr_table_setn(r->subprocess_env, "REQUEST_METHOD", r->method); /* * Provide a special method for modules to communicate * more informative (than the plain canned) messages to us. * Propagate them to ErrorDocuments via the ERROR_NOTES variable: */ if ((error_notes = apr_table_get(r->notes, "error-notes")) != NULL) { apr_table_setn(r->subprocess_env, "ERROR_NOTES", error_notes); } r->method = apr_pstrdup(r->pool, "GET"); r->method_number = M_GET; ap_internal_redirect(custom_response, r); return; } else { /* * Dumb user has given us a bad url to redirect to --- fake up * dying with a recursive server error... */ recursive_error = HTTP_INTERNAL_SERVER_ERROR; ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Invalid error redirection directive: %s", custom_response); } } ap_send_error_response(r_1st_err, recursive_error); } static apr_table_t * (apr_pool_t *p, apr_table_t *t) { const apr_array_header_t *env_arr = apr_table_elts(t); const apr_table_entry_t *elts = (const apr_table_entry_t *) env_arr->elts; apr_table_t *new = apr_table_make(p, env_arr->nalloc); int i; for (i = 0; i < env_arr->nelts; ++i) { if (!elts[i].key) continue; apr_table_setn(new, apr_pstrcat(p, "REDIRECT_", elts[i].key, NULL), elts[i].val); } return new; } static request_rec *internal_internal_redirect(const char *new_uri, request_rec *r) { int access_status; request_rec *new; if (ap_is_recursion_limit_exceeded(r)) { ap_die(HTTP_INTERNAL_SERVER_ERROR, r); return NULL; } new = (request_rec *) apr_pcalloc(r->pool, sizeof(request_rec)); new->connection = r->connection; new->server = r->server; new->pool = r->pool; /* * A whole lot of this really ought to be shared with http_protocol.c... * another missing cleanup. It's particularly inappropriate to be * setting header_only, etc., here. */ new->method = r->method; new->method_number = r->method_number; new->allowed_methods = ap_make_method_list(new->pool, 2); ap_parse_uri(new, new_uri); new->request_config = ap_create_request_config(r->pool); new->per_dir_config = r->server->lookup_defaults; new->prev = r; r->next = new; /* Must have prev and next pointers set before calling create_request * hook. */ ap_run_create_request(new); /* Inherit the rest of the protocol info... */ new->the_request = r->the_request; new->allowed = r->allowed; new->status = r->status; new->assbackwards = r->assbackwards; new->header_only = r->header_only; new->protocol = r->protocol; new->proto_num = r->proto_num; new->hostname = r->hostname; new->request_time = r->request_time; new->main = r->main; new->headers_in = r->headers_in; new->headers_out = apr_table_make(r->pool, 12); new->err_headers_out = r->err_headers_out; new->subprocess_env = rename_original_env(r->pool, r->subprocess_env); new->notes = apr_table_make(r->pool, 5); new->allowed_methods = ap_make_method_list(new->pool, 2); new->htaccess = r->htaccess; new->no_cache = r->no_cache; new->expecting_100 = r->expecting_100; new->no_local_copy = r->no_local_copy; new->read_length = r->read_length; /* We can only read it once */ new->vlist_validator = r->vlist_validator; new->proto_output_filters = r->proto_output_filters; new->proto_input_filters = r->proto_input_filters; new->output_filters = new->proto_output_filters; new->input_filters = new->proto_input_filters; if (new->main) { /* Add back the subrequest filter, which we lost when * we set output_filters to include only the protocol */ ap_add_output_filter_handle(ap_subreq_core_filter_handle, NULL, new, new->connection); } update_r_in_filters(new->input_filters, r, new); update_r_in_filters(new->output_filters, r, new); apr_table_setn(new->subprocess_env, "REDIRECT_STATUS", apr_itoa(r->pool, r->status)); /* * XXX: hmm. This is because mod_setenvif and mod_unique_id really need * to do their thing on internal redirects as well. Perhaps this is a * misnamed function. */ if ((access_status = ap_run_post_read_request(new))) { ap_die(access_status, new); return NULL; } return new; }