Score:2

.htaccess return error if no RewriteRule meets the request

cn flag

I'm trying my hands on writing a simple REST API and am currently trying to properly configure my .htaccess file.

What I'd like to achieve is the following:

  • have RewriteRules to direct requests towards the actual RestController
  • catch all requests not explicitly served with a RewriteRule before with a FITTING error
    • requests that target any file, subdirectory or file in subdirectory not handled with a former RewriteRule should be answered 403
    • requests that target any non-existent file or subdirectory should be answered 404

What I have achieved up until now is:

  • ✅ serving non-existent files, subdirs and files in subdirs 404
  • ✅ serving existing files and subdirs without proper RewriteRule 403
  • ❌ forwarding valid API requests to the proper endpoint (it stopped working after I added the last 403 rule)
  • ❌ serving 403 for existing files IN SUBDIRS without proper RewriteRule - eg. when I try to access domain.com/subdir/existingfile.pdf, it actually shows the file, which I don't want.

Since subdirs may be created dynamically, I want ALL subdirs and files in them to be handled 403 without having to manually specify them.

This is my current .htaccess:

Options -Indexes
RewriteEngine on
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

# REST mappings
RewriteRule ^json$   RestController.php?key=json [L,nc,qsa]
RewriteRule ^file/(.*)$   RestController.php?key=file&id=$1 [L,nc,qsa]
RewriteRule ^actions$   RestController.php?key=actions&id=none [L,nc,qsa]
RewriteRule ^actions/(.*)$   RestController.php?key=actions&id=$1 [L,nc,qsa]

# catch all non-matched requests and throw 404 or 403 accordingly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ - [R=404,NC,L]
RewriteRule ^ - [R=403,NC,L]
Score:4
kz flag

Because RestController.php is a file and not one of your "mappings", the last rule will unconditionally serve a 403.

You need to either, make an exception for RestController.php and/or use the END flag (new in Apache 2.4), instead of L, to prevent all further processing of the mod_rewrite directives. (The L flag only stops the current round of processing. In a directory context, like .htaccess, the rewritten URL is passed to the next phase of processing and the last rule is then processed during the second phase.)

You also don't need an explicit rule to serve the 404, since that will happen by default. You only need a rule when the request maps to a file (or directory) in order to serve a 403.

For example, with an exception for RestController.php in the last rule block:

:

# catch all non-matched requests and throw 404 or 403 accordingly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule !^RestController\.php$ - [R=403]

# A 404 will naturally be served otherwise...

You don't need the L flag when using a non-3xx status with the R flag.

You don't necessarily need to check for a directory (2nd condition), unless you happen to have a DirectoryIndex document (eg. index.php or index.html) in that directory (but why would you?). A 403 is served automatically by mod_autoindex when requesting a directory with no DirectoryIndex document.

Alternatively, or as well as, use the END flag in the preceding rules:

# REST mappings
RewriteRule ^json$ RestController.php?key=json [END,NC,QSA]
RewriteRule ^file/(.*) RestController.php?key=file&id=$1 [END,NC,QSA]
RewriteRule ^actions$ RestController.php?key=actions&id=none [END,NC,QSA]
RewriteRule ^actions/(.*) RestController.php?key=actions&id=$1 [END,NC,QSA]

# catch all non-matched requests and throw 404 or 403 accordingly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [R=403]

# A 404 will naturally be served otherwise...

The NC flag should not be necessary here. Note also that I removed the $ in ^file/(.*)$ since it's not required (regex is greedy by default).


Aside:

# REST mappings
RewriteRule ^json$ RestController.php?key=json [END,NC,QSA]
RewriteRule ^file/(.*) RestController.php?key=file&id=$1 [END,NC,QSA]
RewriteRule ^actions$ RestController.php?key=actions&id=none [END,NC,QSA]
RewriteRule ^actions/(.*) RestController.php?key=actions&id=$1 [END,NC,QSA]

Your "mappings" could be reduced to a single rule if you standardise the endpoints, ie. Always a key and id parameter and id could be empty (not "none"), which you would need to validate for anyway (both /actions and /actions/ are valid requests according to your rules, but result in different targets). You then validate the target in your PHP script.

For example:

RewriteRule ^(json|file|actions)(?:/(.*))?$ RestController.php?key=$1&id=$2 [END,NC,QSA]

This does mean that /json/<foo> and /file would be successfully rewritten to RestController.php (which would otherwise be ignored by your original rules), but these requests should be handled by your REST API validation anyway. Perhaps .* should really be .+.

traxx2012 avatar
cn flag
Wow, thank you very much for that detailed explanation! I learned a lot about .htaccess today. To be honest, I just cobbled together a few pieces from the web while trying to understand what they do from the documentation... I can't believe I didn't think about the php file actually being a FILE. I went with your suggestion to change the bottom block and (almost) everything does what I wanted and I'm sure I can handle the rest. Thanks again!
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.