diff mbox series

[RFC] Add a prctl to disable ".." traversal in path resolution

Message ID 20241211142929.247692-1-mjg59@srcf.ucam.org (mailing list archive)
State New
Headers show
Series [RFC] Add a prctl to disable ".." traversal in path resolution | expand

Commit Message

Matthew Garrett Dec. 11, 2024, 2:29 p.m. UTC
Path traversal attacks remain a common security vulnerability
(https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=%22path+traversal%22)
and many are due to either failing to filter out ".." when validating a
path or incorrectly collapsing other sequences of "."s into ".." .
Evidence suggests that improving education isn't fixing the problem.

The majority of internet-facing applications are unlikely to require the
ability to handle ".." in a path after startup, and many are unlikely to
require it at all. This patch adds a prctl() to simply request that the
VFS path resolution code return -EPERM if it hits a ".." in the process.
Applications can either call this themselves, or a service manager can
do this on their behalf before execing them.

Note that this does break resolution of symlinks with ".." in them,
which means it breaks the common case of /etc/whatever/sites-available.d
containing site-specific configuration, with
/etc/whatever/sites-enabled.d containing a set of relative symlinks to
../sites-available.d/ entries. In this case either configuration would
need to be updated before deployment, or the process would call prctl()
itself after parsing configuration (and then disable and re-enable the
feature whenever re-reading configuration). Getting this right for all
scenarios involving symlinks seems awkward and I'm not sure it's worth
it, but also I don't even play a VFS expert on TV so if someone has
clever ideas here we can extend this to support that case.

Signed-off-by: Matthew Garrett <mjg59@srcf.ucam.org>
---
 fs/namei.c                 | 7 +++++++
 include/linux/sched.h      | 1 +
 include/uapi/linux/prctl.h | 3 +++
 kernel/sys.c               | 5 +++++
 4 files changed, 16 insertions(+)

Comments

Nicolas Frattaroli Dec. 11, 2024, 3:51 p.m. UTC | #1
On Mittwoch, 11. Dezember 2024 15:29:29 Mitteleuropäische Normalzeit Matthew Garrett wrote:
> Path traversal attacks remain a common security vulnerability
> (https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=%22path+traversal%22)
> and many are due to either failing to filter out ".." when validating a
> path or incorrectly collapsing other sequences of "."s into ".." .
> Evidence suggests that improving education isn't fixing the problem.

Hi Matthew,

I get the motivation here, but I think you've just turned "educate about
handling .. properly" into "educate about this specific mitigation".

Linux already offers a multitude of ways in which people who deploy
potentially buggy applications can restrict which files these applications
have access to, from old fashioned UNIX permissions over SELinux and other
LSMs to namespaces. (Some software, like postfix, even thinks chroots are
appropriate for this, but I think modern understanding of security would
disagree with that.)

People who fall victim to path traversal vulnerabilities in actual
deployments will not have deployed any of those mitigations, and therefore
I don't see why they would deploy this one.

As sad as it is to see that supposed enterprise security network appliances
still fall victim to the same mistakes people's CGI perl scripts did in
the 90s, I don't think the ones who would opt into this mechanism are the
ones getting got through path traversal.

> The majority of internet-facing applications are unlikely to require the
> ability to handle ".." in a path after startup, and many are unlikely to
> require it at all.

I would like to see some evidence of that. Do webservers already realname
the path when a client asks for example.com/foo/../css/bar.css? If so,
there's no path traversal vulnerability, if not, then they do require it.

Cheers,
Nicolas Frattaroli
Aleksa Sarai Dec. 11, 2024, 3:56 p.m. UTC | #2
On 2024-12-11, Matthew Garrett <mjg59@srcf.ucam.org> wrote:
> Path traversal attacks remain a common security vulnerability
> (https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=%22path+traversal%22)
> and many are due to either failing to filter out ".." when validating a
> path or incorrectly collapsing other sequences of "."s into ".." .
> Evidence suggests that improving education isn't fixing the problem.

I was thinking about adding a RESOLVE_NO_DOTDOT which would do something
like this but on a per-openat2-call basis.

The main problem with making this global for the entire process is that
most tools would not be able to practically enable this for themselves
as it would require auditing the entire execution environment as well as
all dependencies that might dare to use ".." in a path anywhere in their
codebase.

Given that "nosymfollow" was added by Google because of the
impossibility of a similar kind of audit, I feel enabling this would
face a pretty similar issue. I get that returning an error (even if it
kills the program) could be seen as preferable in some cases, but as a
general-purpose tool I don't really see how any program could enable
this without fear of issues. Even if the application itself handles
errors gracefully, there's no way of knowing whether some dependency (or
even the stdlib / runtime) will abort the program if it gets an error.

> The majority of internet-facing applications are unlikely to require the
> ability to handle ".." in a path after startup, and many are unlikely to
> require it at all. This patch adds a prctl() to simply request that the
> VFS path resolution code return -EPERM if it hits a ".." in the process.
> Applications can either call this themselves, or a service manager can
> do this on their behalf before execing them.

I would suggest making this -EXDEV to match the rest of the RESOLVE_*
flags (in particular, RESOLVE_BENEATH).

> Note that this does break resolution of symlinks with ".." in them,
> which means it breaks the common case of /etc/whatever/sites-available.d
> containing site-specific configuration, with
> /etc/whatever/sites-enabled.d containing a set of relative symlinks to
> ../sites-available.d/ entries. In this case either configuration would
> need to be updated before deployment, or the process would call prctl()
> itself after parsing configuration (and then disable and re-enable the
> feature whenever re-reading configuration). Getting this right for all
> scenarios involving symlinks seems awkward and I'm not sure it's worth
> it, but also I don't even play a VFS expert on TV so if someone has
> clever ideas here we can extend this to support that case.

I think RESOLVE_BENEATH is usually more along the lines of what programs
that are trying to restrict themselves would want (RESOLVE_IN_ROOT is
what extraction tools want, on the other hand) as it only blocks ".."
components that move you out of the directory you expect.

It also blocks absolute symlinks, which this proposal does nothing about
(it even blocks magic-links, which can be an even bigger issue depending
on what kind of program we are talking about). Alas, RESOLVE_BENEATH
requires education...

> Signed-off-by: Matthew Garrett <mjg59@srcf.ucam.org>
> ---
>  fs/namei.c                 | 7 +++++++
>  include/linux/sched.h      | 1 +
>  include/uapi/linux/prctl.h | 3 +++
>  kernel/sys.c               | 5 +++++
>  4 files changed, 16 insertions(+)
> 
> diff --git a/fs/namei.c b/fs/namei.c
> index 9d30c7aa9aa6..01d0fa415b64 100644
> --- a/fs/namei.c
> +++ b/fs/namei.c
> @@ -2431,6 +2431,13 @@ static int link_path_walk(const char *name, struct nameidata *nd)
>  
>  		switch(lastword) {
>  		case LAST_WORD_IS_DOTDOT:
> +			/*
> +			 * Deny .. in resolution if the process has indicated
> +			 * it wants to protect against path traversal
> +			 * vulnerabilities
> +			 */
> +			if (unlikely(current->deny_path_traversal))
> +				return -EPERM;
>  			nd->last_type = LAST_DOTDOT;
>  			nd->state |= ND_JUMPED;
>  			break;
> diff --git a/include/linux/sched.h b/include/linux/sched.h
> index d380bffee2ef..9fc7f4c11645 100644
> --- a/include/linux/sched.h
> +++ b/include/linux/sched.h
> @@ -1008,6 +1008,7 @@ struct task_struct {
>  	/* delay due to memory thrashing */
>  	unsigned                        in_thrashing:1;
>  #endif
> +	unsigned                        deny_path_traversal:1;
>  #ifdef CONFIG_PREEMPT_RT
>  	struct netdev_xmit		net_xmit;
>  #endif
> diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
> index 5c6080680cb2..d289acecef6c 100644
> --- a/include/uapi/linux/prctl.h
> +++ b/include/uapi/linux/prctl.h
> @@ -353,4 +353,7 @@ struct prctl_mm_map {
>   */
>  #define PR_LOCK_SHADOW_STACK_STATUS      76
>  
> +/* Block resolution of "../" in paths, returning -EPERM instead */
> +#define PR_SET_PATH_TRAVERSAL_BLOCK      77
> +
>  #endif /* _LINUX_PRCTL_H */
> diff --git a/kernel/sys.c b/kernel/sys.c
> index c4c701c6f0b4..204ea88d5597 100644
> --- a/kernel/sys.c
> +++ b/kernel/sys.c
> @@ -2809,6 +2809,11 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
>  			return -EINVAL;
>  		error = arch_lock_shadow_stack_status(me, arg2);
>  		break;
> +	case PR_SET_PATH_TRAVERSAL_BLOCK:
> +		if ((arg2 > 1) || arg3 || arg4 || arg5)
> +			return -EINVAL;
> +		current->deny_path_traversal = !!arg2;
> +		break;
>  	default:
>  		error = -EINVAL;
>  		break;
> -- 
> 2.47.0
> 
>
Al Viro Dec. 11, 2024, 4:20 p.m. UTC | #3
On Thu, Dec 12, 2024 at 02:56:59AM +1100, Aleksa Sarai wrote:

> I think RESOLVE_BENEATH is usually more along the lines of what programs
> that are trying to restrict themselves would want (RESOLVE_IN_ROOT is
> what extraction tools want, on the other hand) as it only blocks ".."
> components that move you out of the directory you expect.
> 
> It also blocks absolute symlinks, which this proposal does nothing about
> (it even blocks magic-links, which can be an even bigger issue depending
> on what kind of program we are talking about). Alas, RESOLVE_BENEATH
> requires education...

So does this prctl, when you get to that - any references to "service manager"
that might turn it on are contradicted by the "after startup" bit in the
original posting.

IOW, I very much doubt that this problem is amenable to cargo-culting.

_If_ somebody wants to collect actual information about the use patterns,
something like prctl that would spew a stack trace when running into
.. would be an obvious approach, but I would strongly object to even
inserting a tracepoint of that sort into the mainline kernel.
Christian Brauner Dec. 13, 2024, 12:55 p.m. UTC | #4
On Thu, Dec 12, 2024 at 02:56:59AM +1100, Aleksa Sarai wrote:
> On 2024-12-11, Matthew Garrett <mjg59@srcf.ucam.org> wrote:
> > Path traversal attacks remain a common security vulnerability
> > (https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=%22path+traversal%22)
> > and many are due to either failing to filter out ".." when validating a
> > path or incorrectly collapsing other sequences of "."s into ".." .
> > Evidence suggests that improving education isn't fixing the problem.
> 
> I was thinking about adding a RESOLVE_NO_DOTDOT which would do something
> like this but on a per-openat2-call basis.

That's what I was thinking a while ago. I discussed that with Linus
in connection to a change by Jann for looking up module paths.

https://lore.kernel.org/r/CAADWXX_zpqzYdCpmQGF3JgsN4+wk3AsuQLCKREkDC1ScxSfDjQ@mail.gmail.com

> 
> The main problem with making this global for the entire process is that
> most tools would not be able to practically enable this for themselves
> as it would require auditing the entire execution environment as well as
> all dependencies that might dare to use ".." in a path anywhere in their
> codebase.

I agree.
Aleksa Sarai Dec. 13, 2024, 1:50 p.m. UTC | #5
On 2024-12-11, Al Viro <viro@zeniv.linux.org.uk> wrote:
> On Thu, Dec 12, 2024 at 02:56:59AM +1100, Aleksa Sarai wrote:
> 
> > I think RESOLVE_BENEATH is usually more along the lines of what programs
> > that are trying to restrict themselves would want (RESOLVE_IN_ROOT is
> > what extraction tools want, on the other hand) as it only blocks ".."
> > components that move you out of the directory you expect.
> > 
> > It also blocks absolute symlinks, which this proposal does nothing about
> > (it even blocks magic-links, which can be an even bigger issue depending
> > on what kind of program we are talking about). Alas, RESOLVE_BENEATH
> > requires education...
> 
> So does this prctl, when you get to that - any references to "service manager"
> that might turn it on are contradicted by the "after startup" bit in the
> original posting.
> 
> IOW, I very much doubt that this problem is amenable to cargo-culting.

I'm not sure I understand what you're saying -- was this comment
intended for me or Matthew? I was just trying to say that:

 1. Most programs that want to access config files or static files to
	serve as a web server where there might be ".." symlinks would
	probably want RESOLVE_BENEATH behaviour (using the config directory
	as the root) because that will only block escaping ".."s (and will
	also block absolute symlinks as well as magic-links that can escape
	too -- which are also issues that you need to deal with anyway).

 2. Blocking this on a process-wide level could cause issues for any
    given program because the language's stdlib or runtime could
	internally do a ".." open operation, and if that fails the runtime
	might decide to crash the program (we've run into issues like this
	in Go -- though those were related to errors from pthread_*
	functions). If the service manager sets the prctl then if the link
	loader or glibc on startup have to resolve ".." due to the way the
	system was set up, you would also get errors (and probably a crash).

	Obviously, some users would prefer the application crash rather than
	resolving a "..", but most would not which is why I'm unsure of how
	much this will help solve the problem in practice.

As a devils-advocate proposal, I wonder if we could instead do something
like nosymfollow (so nodotdot?) where you can mark particular mounts
to have this restriction. This could be used to limit all ".."s globally
for a process using a mntns, but it could also be scoped to application
data directories (or to all untrusted directories) without requiring
program changes (the same argument was used for adding nosymfollow when
RESOLVE_NO_SYMLINKS existed).

Obviously there would be problems with this proposal -- unlike
nosymfollow (which is more like nodev or noexec in concept, and so is
more about conceptually restricting access modes of inodes -- even
though nosymfollow is piped through namei), this is would be nd->mnt
impacting how lookups work, which seems kinda ugly and would burn
another MS_ flag (the final one aside from internal ones AFAICS)...

Just food for thought. (I think LOOKUP_NO_DOTDOT / RESOLVE_NO_DOTDOT is
the most usable solution for most programs that really need this, fwiw.)

> _If_ somebody wants to collect actual information about the use patterns,
> something like prctl that would spew a stack trace when running into
> .. would be an obvious approach, but I would strongly object to even
> inserting a tracepoint of that sort into the mainline kernel.

You probably don't need a custom tracepoint, I expect they can get most
of the information they'd need with bpftrace.
diff mbox series

Patch

diff --git a/fs/namei.c b/fs/namei.c
index 9d30c7aa9aa6..01d0fa415b64 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -2431,6 +2431,13 @@  static int link_path_walk(const char *name, struct nameidata *nd)
 
 		switch(lastword) {
 		case LAST_WORD_IS_DOTDOT:
+			/*
+			 * Deny .. in resolution if the process has indicated
+			 * it wants to protect against path traversal
+			 * vulnerabilities
+			 */
+			if (unlikely(current->deny_path_traversal))
+				return -EPERM;
 			nd->last_type = LAST_DOTDOT;
 			nd->state |= ND_JUMPED;
 			break;
diff --git a/include/linux/sched.h b/include/linux/sched.h
index d380bffee2ef..9fc7f4c11645 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1008,6 +1008,7 @@  struct task_struct {
 	/* delay due to memory thrashing */
 	unsigned                        in_thrashing:1;
 #endif
+	unsigned                        deny_path_traversal:1;
 #ifdef CONFIG_PREEMPT_RT
 	struct netdev_xmit		net_xmit;
 #endif
diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
index 5c6080680cb2..d289acecef6c 100644
--- a/include/uapi/linux/prctl.h
+++ b/include/uapi/linux/prctl.h
@@ -353,4 +353,7 @@  struct prctl_mm_map {
  */
 #define PR_LOCK_SHADOW_STACK_STATUS      76
 
+/* Block resolution of "../" in paths, returning -EPERM instead */
+#define PR_SET_PATH_TRAVERSAL_BLOCK      77
+
 #endif /* _LINUX_PRCTL_H */
diff --git a/kernel/sys.c b/kernel/sys.c
index c4c701c6f0b4..204ea88d5597 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -2809,6 +2809,11 @@  SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 			return -EINVAL;
 		error = arch_lock_shadow_stack_status(me, arg2);
 		break;
+	case PR_SET_PATH_TRAVERSAL_BLOCK:
+		if ((arg2 > 1) || arg3 || arg4 || arg5)
+			return -EINVAL;
+		current->deny_path_traversal = !!arg2;
+		break;
 	default:
 		error = -EINVAL;
 		break;