consistent clean_path_info separator - #2257
akostadinov wants to merge 1 commit into
Conversation
It is inconsistent to prepend `/` while earlier `clean.join(::File::SEPARATOR)`. Additionally request `PATH_INFO` should always start with a slash so we can remove the conditional prepending and just always prepend. This can also prevent path traversal attacks in case some code checks the request path beginning using string comparison or regular expression but the underlying server implementation allowed a request path without a leading slash through. I can say that at least puma fails fast on request paths without a leading slash but I see no harm in being extra sure.
| clean_path = clean.join(::File::SEPARATOR) | ||
| clean_path.prepend("/") if parts.empty? || parts.first.empty? | ||
| clean_path | ||
| "#{::File::SEPARATOR}#{clean.join(::File::SEPARATOR)}" |
There was a problem hiding this comment.
This results in different behavior for relative paths (e.g. "a", "b/a"). Previously, clean_path_info did not prepend a slash, and now it would. For that reason, I doubt it would be acceptable.
Looking at the history, it appears to be called with PATH_INFO, which generally should be empty or start with a slash. However, the specs have tests with relative paths. Those tests happen to work with this, but that seems like an accident and not deliberate. For example, there is a test for "test/..", but not a test for "test" by itself.
As we cannot be sure how users are using clean_path_info, I don't think we should accept the behavior change.
There was a problem hiding this comment.
Could you say in what case PATH_INFO would be empty or relative (I couldn't reproduce this in practice with puma)? What valid HTTP request will make it empty?
I think it is important for this method to be as safe as possible for the intended purpose - to make the PATH_INFO predictable to compare or use regular expression on. Rather than some unintended use cases.
Presently just figuring out what would be the actual path of a request is rather hard. And it should not be hard to do it right.
This means, as I mentioned to prevent https://owasp.org/www-community/attacks/Path_Traversal
There was a problem hiding this comment.
HTTP 1.1 supports only (https://datatracker.ietf.org/doc/html/rfc9112#name-request-target)
- absolute path
- full URL (in which case server sets PATH_INFO to an absolute path)
- host:port (in CONNECT requests but we are not a proxy)
*(single star for server global OPTIONS request, but we can handle this as an individual check)
So basically I see no valid use cases where the result should be a relative path.
There was a problem hiding this comment.
Just to clarify, we do check those four scenarios here:
Lines 364 to 389 in fc541f3
There was a problem hiding this comment.
@ioquatix , I wonder why empty string is accepted. RFC clearly reads:
If the target URI's path component is empty, the client MUST send "/" as the path within the origin-form of request-target.
There was a problem hiding this comment.
Additionally the RFC states:
Recipients of an invalid request-line SHOULD respond with either a 400 (Bad Request) error or a 301 (Moved Permanently) redirect with the request-target properly encoded. A recipient SHOULD NOT attempt to autocorrect and then process the request without a redirect, since the invalid request-line might be deliberately crafted to bypass security filters along the request chain.
Which is to a big extend my concern.
|
We can check the reason why this method exists: 9a74ba3 The point is to convert a The usage is something like this: @path = F.join(@root, clean_path_info)There are some issues IMHO:
I find it odd that it returns what effectively looks like an absolute path, yet it's treated as a relative path. I think this is error prone, e.g. File.join("/srv/http/mysite/public", "/background.png")
# => "/srv/http/mysite/public/background.png"
File.expand_path("/background.png", "/srv/http/mysite/public")
# => "/background.png"In other words, it looks easy to make a mistake and get the wrong behaviour, IOW, I think the way the method works is inherently unsafe. I think the safe solution is to pass in the full root, e.g. Rack::Utils.full_path(root, env["PATH_INFO"]) -> file pathFor security reasons, we should assert that the result still has To clarify, I think there are some things we should do:
|
|
I'm going to close this PR in favour of maybe doing something in #2302 |
It is inconsistent to prepend
/while earlierclean.join(::File::SEPARATOR).Additionally request
PATH_INFOshould always start with a slash so wecan remove the conditional prepending and just always prepend.
This can also prevent path traversal attacks in case some code checks
the request path beginning using string comparison or regular expression
but the underlying server implementation allowed a request path without
a leading slash through.
I can say that at least puma fails fast on request paths without a
leading slash but I see no harm in being extra sure.
P.S. actually I'd be even happier to make it
join("/")instead ofclean.join(::File::SEPARATOR). Is there any arch that would need a different separator than/?