Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/hex_core.erl
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
%% `134_217_728' (128 MiB). Set to `infinity' to not enforce the limit.
%%
%% * `tarball_files_root' - Root directory for source files when creating tarballs.
%% Required for filesystem source paths, which must be relative and must resolve inside
%% this root after following symlinks. Set to `undefined' when all tarball contents are
%% provided as binaries and no filesystem source paths are used (default: `undefined').
%% Filesystem source paths must resolve inside this root after following symlinks.
%% Relative source paths are resolved from this root and absolute source paths must be
%% inside it (default: `"."').
%%
%% * `docs_tarball_max_size' - Maximum size of docs tarball, defaults to
%% `16_777_216' (16 MiB). Set to `infinity' to not enforce the limit.
Expand Down Expand Up @@ -118,7 +118,7 @@
repo_verify => boolean(),
repo_verify_origin => boolean(),
send_100_continue => boolean(),
tarball_files_root => file:filename() | undefined,
tarball_files_root => file:filename(),
tarball_max_size => pos_integer() | infinity,
tarball_max_uncompressed_size => pos_integer() | infinity,
docs_tarball_max_size => pos_integer() | infinity,
Expand Down Expand Up @@ -146,7 +146,7 @@ default_config() ->
repo_verify => true,
repo_verify_origin => true,
send_100_continue => true,
tarball_files_root => undefined,
tarball_files_root => ".",
tarball_max_size => 16 * 1024 * 1024,
tarball_max_uncompressed_size => 128 * 1024 * 1024,
docs_tarball_max_size => 16 * 1024 * 1024,
Expand Down
83 changes: 64 additions & 19 deletions src/hex_tarball.erl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ create(Metadata, Files, Config) ->
tarball_max_size := TarballMaxSize,
tarball_max_uncompressed_size := TarballMaxUncompressedSize
} = Config,
FilesRoot = maps:get(tarball_files_root, Config, undefined),
FilesRoot = maps:get(tarball_files_root, Config, "."),

MetadataBinary = encode_metadata(Metadata),

Expand Down Expand Up @@ -141,7 +141,7 @@ create_docs(Files, Config) ->
docs_tarball_max_size := TarballMaxSize,
docs_tarball_max_uncompressed_size := TarballMaxUncompressedSize
} = Config,
FilesRoot = maps:get(tarball_files_root, Config, undefined),
FilesRoot = maps:get(tarball_files_root, Config, "."),

case validate_create_files(Files, FilesRoot) of
{ok, ValidatedFiles} ->
Expand Down Expand Up @@ -354,8 +354,6 @@ format_error({tarball, {too_big_compressed, Size}}) ->
io_lib:format("package exceeds max compressed size ~w ~s", [format_byte_size(Size), "MB"]);
format_error({tarball, {missing_files, Files}}) ->
io_lib:format("missing files: ~p", [Files]);
format_error({tarball, missing_files_root}) ->
"tarball files root is required when creating tarballs from filesystem paths";
format_error({tarball, {bad_version, Vsn}}) ->
io_lib:format("unsupported version: ~p", [Vsn]);
format_error({tarball, invalid_checksum}) ->
Expand Down Expand Up @@ -939,25 +937,69 @@ validate_archive_path(Filename) ->
end.

validate_source_file(ArchiveName, SourcePath, FilesRoot) ->
case validate_source_path(SourcePath) of
ok -> validate_source_file_root(ArchiveName, SourcePath, FilesRoot);
{error, _} = Error -> Error
case source_file_paths(SourcePath, FilesRoot) of
{ok, DiskPath, RelativePath, Root} ->
validate_source_file_root(ArchiveName, DiskPath, RelativePath, Root);
outside_root ->
{error, {tarball, {unsafe_path, ArchiveName}}}
end.

validate_source_path(SourcePath) ->
case safe_relative_archive_path(SourcePath) of
false -> {error, {tarball, {unsafe_path, SourcePath}}};
true -> ok
source_file_paths(SourcePath, FilesRoot) ->
Root = normalize_root(filename:absname(FilesRoot)),
case source_relative_path(SourcePath, Root) of
{ok, RelativePath} ->
{ok, source_disk_path(SourcePath, Root), RelativePath, Root};
outside_root ->
outside_root
end.

normalize_root(Path) ->
filename:join(normalize_root_parts(filename:split(Path), [])).

normalize_root_parts([], Acc) ->
lists:reverse(Acc);
normalize_root_parts(["." | Parts], Acc) ->
normalize_root_parts(Parts, Acc);
normalize_root_parts([".." | Parts], Acc) ->
normalize_root_parent(Parts, Acc);
normalize_root_parts([Part | Parts], Acc) ->
normalize_root_parts(Parts, [Part | Acc]).

normalize_root_parent(Parts, [Root] = Acc) ->
case filename:pathtype(Root) of
relative -> normalize_root_parts(Parts, []);
_ -> normalize_root_parts(Parts, Acc)
end;
normalize_root_parent(Parts, [_Part | Acc]) ->
normalize_root_parts(Parts, Acc);
normalize_root_parent(Parts, []) ->
normalize_root_parts(Parts, []).

source_disk_path(SourcePath, Root) ->
case filename:pathtype(SourcePath) of
absolute -> SourcePath;
_ -> filename:join(Root, SourcePath)
end.

validate_source_file_root(_ArchiveName, _SourcePath, undefined) ->
{error, {tarball, missing_files_root}};
validate_source_file_root(ArchiveName, SourcePath, FilesRoot) ->
Root = filename:absname(FilesRoot),
DiskPath = filename:join(Root, SourcePath),
source_relative_path(SourcePath, Root) ->
case filename:pathtype(SourcePath) of
absolute -> strip_root_path(filename:split(SourcePath), filename:split(Root));
_ -> {ok, SourcePath}
end.

strip_root_path([], []) ->
{ok, "."};
strip_root_path(PathParts, []) ->
{ok, filename:join(PathParts)};
strip_root_path([Part | PathParts], [Part | RootParts]) ->
strip_root_path(PathParts, RootParts);
strip_root_path(_PathParts, _RootParts) ->
outside_root.

validate_source_file_root(ArchiveName, DiskPath, RelativePath, Root) ->
case file:read_link_info(DiskPath, []) of
{ok, #file_info{type = Type}} when Type =:= regular; Type =:= directory ->
case validate_source_root(ArchiveName, SourcePath, Root) of
case validate_source_root(ArchiveName, RelativePath, Root) of
ok -> {ok, {ArchiveName, DiskPath}};
{error, _} = Error -> Error
end;
Expand All @@ -968,15 +1010,18 @@ validate_source_file_root(ArchiveName, SourcePath, FilesRoot) ->
false ->
{error, {tarball, {unsafe_symlink, ArchiveName, LinkTarget}}};
true ->
case validate_source_root(ArchiveName, SourcePath, Root) of
case validate_source_root(ArchiveName, RelativePath, Root) of
ok -> {ok, {ArchiveName, DiskPath}};
{error, _} = Error -> Error
end
end;
{ok, #file_info{type = Type}} ->
{error, {tarball, {unsupported_file_type, ArchiveName, Type}}};
_ ->
{ok, {ArchiveName, DiskPath}}
case validate_source_root(ArchiveName, RelativePath, Root) of
ok -> {ok, {ArchiveName, DiskPath}};
{error, _} = Error -> Error
end
end.

validate_source_root(ArchiveName, SourcePath, FilesRoot) ->
Expand Down
74 changes: 67 additions & 7 deletions test/hex_tarball_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -311,24 +311,84 @@ unsafe_paths_to_create_test(Config) ->
OutsideDir = filename:join(BaseDir, "outside"),
ok = file:make_dir(RootDir),
ok = file:make_dir(OutsideDir),
ok = file:make_dir(filename:join(RootDir, "child")),
ok = file:write_file(filename:join(RootDir, "README.md"), <<"README">>),
ok = file:write_file(filename:join(OutsideDir, "secret.txt"), <<"secret">>),
ok = file:make_symlink("../outside", filename:join(RootDir, "link")),
CreateConfig = maps:put(tarball_files_root, RootDir, hex_core:default_config()),
RootReadme = filename:join(RootDir, "README.md"),
{error, {tarball, missing_files_root}} =
hex_tarball:create(Metadata, [{"README.md", "README.md"}]),
{error, {tarball, missing_files_root}} =
hex_tarball:create_docs([{"README.md", "README.md"}]),
{error, {tarball, {unsafe_path, RootReadme}}} =
DotDotConfig =
maps:put(
tarball_files_root, filename:join([RootDir, "child", ".."]), hex_core:default_config()
),
RootReadme = filename:absname(filename:join(RootDir, "README.md")),
OutsideSecret = filename:absname(filename:join(OutsideDir, "secret.txt")),
{ok, Cwd} = file:get_cwd(),
try
ok = file:set_cwd(RootDir),
{ok, _} = hex_tarball:create(Metadata, [{"README.md", "README.md"}]),
{ok, _} = hex_tarball:create_docs([{"README.md", "README.md"}]),
{ok, _} = hex_tarball:create(Metadata, [{"README.md", RootReadme}]),
{ok, _} = hex_tarball:create_docs([{"README.md", RootReadme}]),
{error, {tarball, {unsafe_path, "secret.txt"}}} =
hex_tarball:create(Metadata, [{"secret.txt", OutsideSecret}]),
{error, {tarball, {unsafe_path, "secret.txt"}}} =
hex_tarball:create_docs([{"secret.txt", OutsideSecret}])
after
ok = file:set_cwd(Cwd)
end,
{ok, _} =
hex_tarball:create(
Metadata,
[{"README.md", RootReadme}],
CreateConfig
),
{error, {tarball, {unsafe_path, RootReadme}}} =
{ok, _} =
hex_tarball:create_docs(
[{"README.md", RootReadme}],
CreateConfig
),
{ok, _} =
hex_tarball:create(
Metadata,
[{"README.md", RootReadme}],
DotDotConfig
),
{ok, _} =
hex_tarball:create_docs(
[{"README.md", RootReadme}],
DotDotConfig
),
{error, {tarball, {unsafe_path, "secret.txt"}}} =
hex_tarball:create(
Metadata,
[{"secret.txt", OutsideSecret}],
DotDotConfig
),
{error, {tarball, {unsafe_path, "secret.txt"}}} =
hex_tarball:create_docs(
[{"secret.txt", OutsideSecret}],
DotDotConfig
),
{error, {tarball, {unsafe_path, "secret.txt"}}} =
hex_tarball:create(
Metadata,
[{"secret.txt", OutsideSecret}],
CreateConfig
),
{error, {tarball, {unsafe_path, "secret.txt"}}} =
hex_tarball:create_docs(
[{"secret.txt", OutsideSecret}],
CreateConfig
),
{error, {tarball, {unsafe_path, "missing.txt"}}} =
hex_tarball:create(
Metadata,
[{"missing.txt", "../outside/missing.txt"}],
CreateConfig
),
{error, {tarball, {unsafe_path, "missing.txt"}}} =
hex_tarball:create_docs(
[{"missing.txt", "../outside/missing.txt"}],
CreateConfig
),
{ok, _} =
Expand Down
Loading