The rule
The advice to not leak just any unchecked error message it's good, but it's not supposed to be taken as if it means that you should avoid sending any error message.
The main target for the "don't leak errors" advice is about unchecked error messages and exceptions details such as stack traces, because these can reveal things you don't want to reveal.
Any error message you write yourself (in its entirety) is perfectly fine to return. This of course assumes you don't go and willfully expose things you'd rather keep secret in those messages.
Some error states are sufficiently informative using only a HTTP status code, such as a 404.
I also tend to let more error information bleed through when in DEBUG mode. Since debug mode is only used by devs working locally, this means that outsiders don't have access to it.
Codes
Are codes considered better than informative messages? (Seems like security through obscurity)
The mapped message behind a code is effectively a vetted message, so it is safe in the sense of not spilling unintentional details to the end user.
The issue with codes is that the kind of code you'd want to use is exactly the opposite of being human-friendly, and this is going to impact your developers, who are going to have to look up all these intentionally counterintuitive codes.
What you can do, however, is log your complete stack trace (however you choose to do your logging: file, table, ...), and make sure to include a generated unique identifier in that log entry. Then you can pass this identifier to the end user, telling them something along the lines of "An error occurred. For more help, contact the developers and mention this reference: XXXX"
This makes sure that the end user's don't see the internal details, but they are able to direct a developer towards the correct log entry that contains all of the details, which developers are interested in (and allowed to see).
Example
This is just an example on how I approach error handling. This is by no means the only way to do it. I just wanted to add what I think strikes a reasonable balance between information and security.
- I have a
BusinessException
class. I throw this to indicate that the message is allowed to be passed to the user. There are further derived classes from this, such as ValidationException
or UserPermissionsException
.
- I put a filter on my api controllers, which catches every exception that bubbles up.
- The exception handling logic checks the exception type and acts appropriately:
ValidationException
- Return message, code 400
UserPermissionsException
- Return message, code 403
- Any not already unspecified
BusinessException
- Return message, code 500
NotFoundException
- No message, code 404
- Else, for any other exception:
- If DEBUG: Return message, code 500
- If RELEASE: Return default "an error has occurred" message, code 500
Notice how in DEBUG, business exceptions are no different from regular exceptions. But in RELEASE, business exceptions remain informative (because they have been vetted by the developers) whereas any unexpected exception is assumed to not have been vetted and is therefore obfuscated.
This setup means that right from the get go, my codebase catches and handles all exceptions, but by default only the last step exists so the real information is obscured when in release mode.
And then you expand the handling logic with more specific exception types, so that when you are developing your application/domain/... logic, you have a way of returning an error message to the user by using a custom exception type that the filter can handle.
This requires discipline to use the appropriate exception type. There's no way to technically stop an incompetent or malevolent developer from doing things like throw new ValidationException() { Message = myLeakyException.ToString() }
.