Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two Security Improvements #43

Merged
merged 4 commits into from
Jan 19, 2024

Conversation

iteratee
Copy link
Contributor

First, allow a user to receive an io_device for the newly opened file. This mirrors the behavior of mkstemp in standard libraries.

Second, when creating the base temporary directories, ensure that the directory is newly created. We could do this once in the server, but I did it new for each pid.
Cleanup these temporary directories on pid shutdown.

Includes tests for both features.

@benwilson512
Copy link
Member

Hi @iteratee thanks for the PR. Can you elaborate a bit on how these measures improve security?

@iteratee
Copy link
Contributor Author

Hi @iteratee thanks for the PR. Can you elaborate a bit on how these measures improve security?

Sure. The first one mirrors the behavior of (mkstemp)[https://www.gnu.org/software/libc/manual/html_node/Temporary-Files.html#index-mkstemp]. Although with the default behavior in Briefly a file is created exclusively, because a reference to that opened file is not returned to the caller, only the path, there is no guarantee that when the caller opens the file, it will be the same file. It's called a (TOCTOU)[https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use] vulnerability. By returning the pid, it is the equivalent to returning the file descriptor in C.

The second change mirrors (mkdtemp)[https://www.gnu.org/software/libc/manual/html_node/Temporary-Files.html#index-mkdtemp], but for the temporary directory that briefly creates, or ensures exists, the first time a pid asks for a temporary file. It isn't necessary for security to re-create it every time, it could be done once at startup and managed by the genserver. But the existing code possibly created a new directory for each pid, so I kept that paradigm.

The security part of the temporary directory change is that the directory may already exist and be owned by another user, or may be a symlink to some other place. The code now makes sure that the last component of the temporary directory is newly created. This prevents it from being owned by another user. It also changes the mode to 0700. I looked through the erlang source, there isn't a way to pass the mode to the mkdir call. It will create it 0755 & (~umask), but we can change it 0700 afterword.

Tracking the temporary directory that a file belongs to and attempting optimistic cleanup isn't strictly necessary for security, but goes along with the idea of Briefly. When we clean up for a pid, we attempt to remove the temporary directory created for that pid. We only want to remove it if it is completely empty. It may not be empty if we have given away a temporary file that is still in use. When cleanup is executed for a pid, if it has gifted files, cleanup attempts to remove the temporary directory for the gifted file. It only does this if the pid that created the file has already been cleaned up. The cleanup of directories uses rmdir which doesn't succeed unless the directory is empty.

@iteratee
Copy link
Contributor Author

@benwilson512 Ping? I noticed I need to adjust the description on one commit. We don't track the pids anymore because they default to closing when the process that opened them closes.

@benwilson512
Copy link
Member

benwilson512 commented Oct 26, 2023

Hi @iteratee thanks for your very helpful explanation. Your thorough test additions are also extremely welcome.

Two quick follow up questions:

  1. Presumably the secure option is mutually exclusive with the directory option? If so, we should perhaps enforce that in create lest people call create(directory: true, secure: true) and expect that something is happening that is not.

  2. I am trying to come up with an option name that is different than :secure. I am perfectly happy to have the security advantages of the option presented in the docs, but I don't necessarily want to imply that not using it is insecure. Whether it is or isn't secure depends on how someone is using the path that is returned, the environment in which the BEAM node in question is operating, and the attack vectors that matter to the end user.

It occurs to me that one way to solve both of these things is to deprecate directory: true and introduce type: type where type would be :path | :directory | :device, default being :path. The docs covering the type option could present (amongst other things) the security considerations relevant to each.

Thoughts?

@iteratee
Copy link
Contributor Author

iteratee commented Jan 5, 2024

Sorry for the long delay. I started down a research rabbit hole of where the "s" in mkstemp came from and what it stands for. I couldn't find any clear sources.

I went ahead and implemented your suggestion. Let me know if there are docs that you think need to be updated.

end
end

defp open(%{directory: true} = options, tmp, attempts, _) when attempts < @max_attempts do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to straight up remove this option and not basically convert it to type: :directory then we need to make this a major version bump since it's a breaking change. We could alternatively note that it is deprecated in the docs, and then convert directory: true to type: :directory in the code and do a minor bump for now, with plans for a major release later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and added back handling of directory: true and added a test that it was still handled. I changed the documentation that I could find all to be type: :directory, so that if you want to drop directory: true in a major release, you can do that.

Sorry that I missed this.

If the temporary directory is world writable, in order to securely open
a temporary file, you need to create it and open it at the same time and
have exclusive access to it. It remains secure as long as you hold onto
the file descriptor returned by open. Requesting a device returns the io
device and the path, rather than just the path.

Note that the io device is owned by the pid that called open(), and will
be closed when that process exits. There is no way to hand off the
device to another pid. If a temporary device needs to be long lived, it
needs to be opened by a process that is at least as long lived.
There is no guarantee that the cleanup will have run before the test
thread receives the monitor notification. Busy wait watching ETS to
ensure that cleanup has had a chance to run.
Just hides one ets usage behind a helper. No other functional changes.
Create a new temporary directory with defined permissions for each pid.
Ensuring that the creation is new prevents race conditions where the
directory already exists. Ideally we would pass the mode when creating
the directory, but that is not possible with the Elixir nor Erlang
standard libraries.

When cleaning up for a pid, optimistically attempt to delete the
temporary directory using rmdir. This won't delete the directory if any
files are still there, which occurs if they have been given away.

When cleaning up for a pid, look for any temporary directories for files
we have been gifted. Remove them if pid in question is no longer using
the temporary directory.

Allow the application to configure the mode for the temporary directory.
Copy link
Member

@benwilson512 benwilson512 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, thank you!

@benwilson512 benwilson512 merged commit 335af37 into CargoSense:master Jan 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants