Repository navigation
avro_idl: confine IDL import paths - #133
Conversation
f1b420f to
a9d5d9d
Compare
42bc10d to
b1e699f
Compare
b1e699f to
01b7900
Compare
|
Thanks for the fix. I used AI for some research, but the ideas below are my personal: One concern about the The fix uses Layouts like this become impossible to load:
The reference Java parser (IdlReader.findImport) supports this implicitly by using URI.resolve with no path restrictions. 😱 . There are lots of pblic auro schemas which do Cross-directory layouts are also an explicit user request in the Avro ecosystem (e.g. sbt-avrohugger#68) — splitting schemas by sub-domain via sibling directories is the normal way to organize a non-trivial schema set. This PR also rewrites the existing I suggest PR is updated to a well-established pattern for this kind of confinement is the web-server "document root" / chroot-style boundary: a fixed root directory is set at the top level, and traversal is permitted freely below it but never above. Apache's DocumentRoot, nginx's root, every static file server works this way because legitimate content layouts use Concretely: track a I understand that such result can be achieved with custom Minor: README |
|
Smth like this seem to work: diff --git a/src/avro_idl.erl b/src/avro_idl.erl
index 47db97eb3b6..68735bd1173 100644
--- a/src/avro_idl.erl
+++ b/src/avro_idl.erl
@@ -18,7 +18,9 @@
-type read_fun() :: fun((Cwd :: string(), Path :: string()) ->
{ok, binary()} | {error, term()}).
--record(st, {cwd, read_fun :: read_fun()}).
+-record(st, {cwd :: string(),
+ rootdir :: string(),
+ read_fun :: read_fun()}).
decode_schema(SchemaStr, Cwd) ->
decode_schema(SchemaStr, Cwd, []).
@@ -42,23 +44,41 @@ decode_schema(SchemaStr, Cwd, Opts) ->
%% @doc Default `read_fun' used when one is not supplied via options.
%% Import paths are resolved relative to the importing file's directory
-%% (`Cwd'). Absolute paths and relative paths that escape `Cwd' through
-%% `..' are refused with `{error, {import_outside_root, Path}}'. Callers
-%% that need different resolution semantics can supply their own
-%% function via the `read_fun' option.
--spec default_read_fun() -> read_fun().
-default_read_fun() ->
+%% (`Cwd'), but the resolved location must stay under `RootDir' — the
+%% top-level caller's `Cwd' unless overridden via the `rootdir' option.
+%% Absolute paths and relative paths that escape `RootDir' through `..'
+%% are refused with `{error, {import_outside_root, Path}}'. `..' within
+%% `RootDir' is allowed, so layouts like `orders/order.avdl' importing
+%% `../common/types.avdl' work as long as `common/' is also under
+%% `RootDir'. Callers that need different resolution semantics can
+%% supply their own function via the `read_fun' option.
+-spec default_read_fun(RootDir :: string()) -> read_fun().
+default_read_fun(RootDir) ->
+ RootParts = filename:split(RootDir),
fun(Cwd, Path) ->
case filename:pathtype(Path) of
- absolute ->
- {error, {import_outside_root, Path}};
+ relative ->
+ CwdParts = filename:split(Cwd),
+ case lists:prefix(RootParts, CwdParts) of
+ true ->
+ RelCwd =
+ case lists:nthtail(length(RootParts), CwdParts) of
+ [] -> ".";
+ Rest -> filename:join(Rest)
+ end,
+ Candidate = filename:join(RelCwd, Path),
+ case filelib:safe_relative_path(Candidate, RootDir) of
+ unsafe ->
+ {error, {import_outside_root, Path}};
+ SafePath ->
+ file:read_file(
+ filename:join(RootDir, SafePath))
+ end;
+ false ->
+ {error, {import_outside_root, Path}}
+ end;
_ ->
- case filelib:safe_relative_path(Path, Cwd) of
- unsafe ->
- {error, {import_outside_root, Path}};
- SafePath ->
- file:read_file(filename:join(Cwd, SafePath))
- end
+ {error, {import_outside_root, Path}}
end
end.
@@ -66,8 +86,9 @@ new_context(Cwd) ->
new_context(Cwd, []).
new_context(Cwd, Opts) ->
- ReadFun = proplists:get_value(read_fun, Opts, default_read_fun()),
- #st{cwd = Cwd, read_fun = ReadFun}.
+ RootDir = proplists:get_value(rootdir, Opts, Cwd),
+ ReadFun = proplists:get_value(read_fun, Opts, default_read_fun(RootDir)),
+ #st{cwd = Cwd, rootdir = RootDir, read_fun = ReadFun}.
str_to_avpr(String, Cwd) ->
str_to_avpr(String, Cwd, []).
@@ -119,11 +140,11 @@ process_imports(Defs, St) ->
[Def]
end, Defs).
-load_idl_import(Path, #st{cwd = Cwd, read_fun = ReadFun} = St) ->
+load_idl_import(Path, #st{cwd = Cwd, rootdir = Root, read_fun = ReadFun} = St) ->
{ok, Bin} = ReadFun(Cwd, Path),
ImportedCwd = filename:dirname(filename:join(Cwd, Path)),
Avpr = str_to_avpr(binary_to_list(Bin), ImportedCwd,
- [{read_fun, ReadFun}]),
+ [{read_fun, ReadFun}, {rootdir, Root}]),
types_from_avpr(Avpr, St).
load_avpr_import(Path, #st{cwd = Cwd, read_fun = ReadFun} = St) ->
diff --git a/src/avro_schema_store.erl b/src/avro_schema_store.erl
index 08be9fca68d..c664e2f092c 100644
--- a/src/avro_schema_store.erl
+++ b/src/avro_schema_store.erl
@@ -40,13 +40,16 @@
-export([ new/0
, new/1
, new/2
+ , new/3
, close/1
, is_store/1
]).
%% Import
-export([ import_file/2
+ , import_file/3
, import_files/2
+ , import_files/3
, import_schema_json/2
, import_schema_json/3
]).
@@ -97,11 +100,19 @@ new(Options) ->
end
end.
-%% @doc Create a new schema store and improt the given schema JSON files.
+%% @doc Create a new schema store and import the given schema JSON files.
-spec new([proplists:property()], [filename()]) -> store().
new(Options, Files) ->
+ new(Options, Files, []).
+
+%% @doc Same as {@link new/2} but forwards `ImportOpts' to
+%% {@link import_files/3} — e.g. `[{rootdir, "schemas"}]' when loading
+%% `.avdl' files that import from sibling directories via `..'.
+-spec new([proplists:property()], [filename()], [proplists:property()]) ->
+ store().
+new(Options, Files, ImportOpts) ->
Store = new(Options),
- import_files(Files, Store).
+ import_files(Files, Store, ImportOpts).
%% @doc Return true if the given arg is a schema store.
-spec is_store(term()) -> boolean().
@@ -120,7 +131,13 @@ to_lookup_fun(Store) ->
%% @doc Import avro JSON files into schema store.
-spec import_files([filename()], store()) -> store().
import_files(Files, Store) ->
- lists:foldl(fun(File, S) -> import_file(File, S) end, Store, Files).
+ import_files(Files, Store, []).
+
+%% @doc Import avro JSON/IDL files into schema store with options.
+%% Options are forwarded to {@link import_file/3}.
+-spec import_files([filename()], store(), [proplists:property()]) -> store().
+import_files(Files, Store, Opts) ->
+ lists:foldl(fun(File, S) -> import_file(File, S, Opts) end, Store, Files).
%% @doc Import avro JSON or IDL file into schema store.
%% For ".avdl" files, the IDL is compiled and all types are imported.
@@ -134,9 +151,22 @@ import_files(Files, Store) ->
%% @end
-spec import_file(filename(), store()) -> store().
import_file(File, Store) ->
+ import_file(File, Store, []).
+
+%% @doc Same as {@link import_file/2} but accepts options.
+%% Supported options for `.avdl' inputs:
+%% <ul>
+%% <li>`{rootdir, Dir}' — directory that bounds default import
+%% resolution. Imports may use `..' freely within `Dir' but
+%% cannot escape it. Defaults to the directory of `File'.</li>
+%% <li>`{read_fun, Fun}' — custom reader; bypasses all path checks.</li>
+%% </ul>
+%% Options are ignored for non-IDL inputs.
+-spec import_file(filename(), store(), [proplists:property()]) -> store().
+import_file(File, Store, Opts) ->
case filename:extension(File) of
Ext when Ext =:= ".avdl"; Ext =:= <<".avdl">> ->
- import_idl_file(File, Store);
+ import_idl_file(File, Store, Opts);
_ ->
case file:read_file(File) of
{ok, Json} ->
@@ -254,11 +284,11 @@ import_schema_json(AssignedName, Json, Store) ->
%% @private Import an Avro IDL (.avdl) file into the schema store.
%% All named types defined in (or imported by) the protocol are added.
--spec import_idl_file(filename(), store()) -> store().
-import_idl_file(File, Store) ->
+-spec import_idl_file(filename(), store(), [proplists:property()]) -> store().
+import_idl_file(File, Store, Opts) ->
Cwd = filename:dirname(File),
{ok, Bin} = file:read_file(File),
- Schema = avro_idl:decode_schema(binary_to_list(Bin), Cwd),
+ Schema = avro_idl:decode_schema(binary_to_list(Bin), Cwd, Opts),
{_Root, FlatTypes} = avro:flatten_type(Schema),
lists:foldl(fun do_add_type/2, Store, FlatTypes).
|
Switch the default read_fun from per-file Cwd confinement to a fixed
rootdir, so "../" is allowed within the rooted tree (e.g. sibling
directories under a shared schemas/ root) but never escapes it. The
rootdir defaults to the top-level caller's Cwd and is overridable via
a new {rootdir, Dir} option, plumbed through avro_idl:decode_schema,
new_context, str_to_avpr, and new avro_schema_store:new/3, import_file/3,
and import_files/3 entry points.
Restores the in-repo subdir/submodule.avdl -> ../foo.avdl example as the
fixture for the legitimate sibling-import pattern.
b0b8da7 to
3b00377
Compare
filelib or filename does not seem to export apis to normalize path-walk of '..', so we implemented it locally. now Root dir and join of Cwd + Path are normalized and compared
No description provided.