File 1081-Validate-initial-options.patch of Package erlang
From 5b15656942ac27b1d706e3297f946e34810ba7e8 Mon Sep 17 00:00:00 2001
From: Raimo Niskanen <raimo@erlang.org>
Date: Tue, 10 Feb 2026 18:13:21 +0100
Subject: [PATCH 1/5] Validate initial options
Ensure that relative path components does not allow
a requested file name to go outside the configured root_dir.
root_dir should be checked to be a directory and absolute.
If root_dir is used, Filename should be checked to be
relative under root_dir.
---
lib/tftp/src/tftp_file.erl | 87 ++++++++++++++++++++++----------------
1 file changed, 50 insertions(+), 37 deletions(-)
diff --git a/lib/tftp/src/tftp_file.erl b/lib/tftp/src/tftp_file.erl
index b6fb97bfb5..3c6883d69a 100644
--- a/lib/tftp/src/tftp_file.erl
+++ b/lib/tftp/src/tftp_file.erl
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2023. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -43,10 +43,6 @@
-include_lib("kernel/include/file.hrl").
--record(initial,
- {filename,
- is_native_ascii}).
-
-record(state,
{access,
filename,
@@ -294,45 +290,62 @@ abort(_Code, _Text, #state{fd = Fd, access = Access} = State) ->
%%-------------------------------------------------------------------
handle_options(Access, Filename, Mode, Options, Initial) ->
- I = #initial{filename = Filename, is_native_ascii = is_native_ascii()},
- {Filename2, IsNativeAscii} = handle_initial(Initial, I),
- IsNetworkAscii = handle_mode(Mode, IsNativeAscii),
+ {Filename2, IsNativeAscii} = handle_initial(Initial, Filename),
+ IsNetworkAscii =
+ case Mode of
+ "netascii" when IsNativeAscii =:= true ->
+ true;
+ "octet" ->
+ false;
+ _ ->
+ throw({error, {badop, "Illegal mode " ++ Mode}})
+ end,
Options2 = do_handle_options(Access, Filename2, Options),
{ok, Filename2, IsNativeAscii, IsNetworkAscii, Options2}.
-handle_mode(Mode, IsNativeAscii) ->
- case Mode of
- "netascii" when IsNativeAscii =:= true -> true;
- "octet" -> false;
- _ -> throw({error, {badop, "Illegal mode " ++ Mode}})
+handle_initial(
+ #state{filename = Filename, is_native_ascii = IsNativeAscii}, _FName) ->
+ {Filename, IsNativeAscii};
+handle_initial(Initial, Filename) when is_list(Initial) ->
+ Opts = get_initial_opts(Initial, #{}),
+ {case Opts of
+ #{ root_dir := RootDir } ->
+ safe_filename(Filename, RootDir);
+ #{} ->
+ Filename
+ end,
+ maps:get(is_native_ascii, Opts, is_native_ascii())}.
+
+get_initial_opts([], Opts) -> Opts;
+get_initial_opts([Opt | Initial], Opts) ->
+ case Opt of
+ {root_dir, RootDir} ->
+ is_map_key(root_dir, Opts) andalso
+ throw({error, {badop, "Internal error. root_dir already set"}}),
+ get_initial_opts(Initial, Opts#{ root_dir => RootDir });
+ {native_ascii, Bool} when is_boolean(Bool) ->
+ get_initial_opts(Initial, Opts#{ is_native_ascii => Bool })
end.
-handle_initial([{root_dir, Dir} | Initial], I) ->
- case catch filename_join(Dir, I#initial.filename) of
- {'EXIT', _} ->
- throw({error, {badop, "Internal error. root_dir is not a string"}});
- Filename2 ->
- handle_initial(Initial, I#initial{filename = Filename2})
- end;
-handle_initial([{native_ascii, Bool} | Initial], I) ->
- case Bool of
- true -> handle_initial(Initial, I#initial{is_native_ascii = true});
- false -> handle_initial(Initial, I#initial{is_native_ascii = false})
- end;
-handle_initial([], I) when is_record(I, initial) ->
- {I#initial.filename, I#initial.is_native_ascii};
-handle_initial(State, _) when is_record(State, state) ->
- {State#state.filename, State#state.is_native_ascii}.
-
-filename_join(Dir, Filename) ->
- case filename:pathtype(Filename) of
- absolute ->
- [_ | RelFilename] = filename:split(Filename),
- filename:join([Dir, RelFilename]);
- _ ->
- filename:join([Dir, Filename])
+safe_filename(Filename, RootDir) ->
+ absolute =:= filename:pathtype(RootDir) orelse
+ throw({error, {badop, "Internal error. root_dir is not absolute"}}),
+ filelib:is_dir(RootDir) orelse
+ throw({error, {badop, "Internal error. root_dir not a directory"}}),
+ RelFilename =
+ case filename:pathtype(Filename) of
+ absolute ->
+ filename:join(tl(filename:split(Filename)));
+ _ -> Filename
+ end,
+ case filelib:safe_relative_path(RelFilename, RootDir) of
+ unsafe ->
+ throw({error, {badop, "Internal error. Filename out of bounds"}});
+ SafeFilename ->
+ filename:join(RootDir, SafeFilename)
end.
+
do_handle_options(Access, Filename, [{Key, Val} | T]) ->
case Key of
"tsize" ->
--
2.51.0