VYPR
Low severityNVD Advisory· Published Jun 10, 2026

CVE-2026-48855

CVE-2026-48855

Description

Erlang OTP's ssh_sftpd module leaks the absolute filesystem path of the SFTP root directory via the SSH_FXP_READLINK handler.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Erlang OTP's ssh_sftpd module leaks the absolute filesystem path of the SFTP root directory via the SSH_FXP_READLINK handler.

Vulnerability

An Exposure of Sensitive Information to an Unauthorized Actor vulnerability exists in the ssh_sftpd module of Erlang OTP. The SSH_FXP_READLINK handler incorrectly sends the raw result of file:read_link/2 to the client without applying the chroot_filename/2 function to strip the backend root prefix. This affects OTP versions from 17.0 before 29.0.2, 28.5.0.2, and 27.3.4.13, corresponding to ssh versions before 6.0.1, 5.5.2.1, and 5.2.11.8 [3].

Exploitation

An authenticated SFTP client can exploit this by creating a symbolic link within the chroot directory that points to the root directory (/). When the client then reads this symlink using SSH_FXP_READLINK, the server resolves the target to the absolute backend root path and returns it to the client, instead of the expected chrooted value [4].

Impact

Successful exploitation allows an attacker to discover the absolute filesystem path of the SFTP root directory and any symlink targets within it. This information disclosure could reveal host directory structures, mount point names, or usernames if the root is located under /home. However, this vulnerability alone does not grant access to file contents, credentials, or paths outside the configured root directory [3, 4].

Mitigation

This issue is fixed in Erlang OTP versions 29.0.2, 28.5.0.2, and 27.3.4.13, and corresponding ssh versions 6.0.1, 5.5.2.1, and 5.2.11.8 [3]. Workarounds include using OS-level chroot to isolate the SFTP server process, ensuring the SFTP port is not reachable from untrusted machines, and avoiding sensitive information in the configured root directory path [4].

AI Insight generated on Jun 10, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

3
  • SSH/SSHreferences
  • Erlang/sshllm-fuzzy
    Range: >=3.0.1 <6.0.1, >=5.5.2.1, >=5.2.11.8
  • Erlang/OTPllm-fuzzy
    Range: >=17.0 <29.0.2, >=28.5.0.2, >=27.3.4.13

Patches

1
8f4224a0d267

Fix absolute path leak from SSH_FXP_READLINK

https://github.com/erlang/otpMichał WąsowskiJun 8, 2026via body-scan
2 files changed · +114 20
  • lib/ssh/src/ssh_sftpd.erl+4 2 modified
    @@ -371,8 +371,10 @@ handle_op(?SSH_FXP_READLINK, ReqId, <<?UINT32(PLen), RelPath:PLen/binary>>,
         {Res, FS1} = FileMod:read_link(AbsPath, FS0),
         case Res of
     	{ok, NewPath} ->
    -	    ssh_xfer:xf_send_name(State#state.xf, ReqId, NewPath,
    -				  #ssh_xfer_attr{type=regular});
    +        AbsTarget = filename:absname(NewPath, filename:dirname(AbsPath)),
    +        ChrootedPath = chroot_filename(canonicalize_filename(AbsTarget), State),
    +        ssh_xfer:xf_send_name(State#state.xf, ReqId, ChrootedPath,
    +                              #ssh_xfer_attr{type=regular});
     	{error, Error} ->
     	    ssh_xfer:xf_send_status(State#state.xf, ReqId,
     				    ssh_xfer:encode_erlang_status(Error))
    
  • lib/ssh/test/ssh_sftpd_SUITE.erl+110 18 modified
    @@ -36,6 +36,7 @@
     -export([
              access_outside_root/1,
              links/1,
    +         links_root/1,
              mk_rm_dir/1,
              open_close_dir/1,
              open_close_file/1,
    @@ -95,6 +96,7 @@ all() ->
          retrieve_attributes, 
          set_attributes, 
          links,
    +     links_root,
          ver3_rename,
          ver3_open_flags,
          relpath, 
    @@ -179,6 +181,14 @@ init_per_testcase(TestCase, Config) ->
     			  SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir},
     								  {sftpd_vsn, 6}])],
     			  ssh:daemon(0, [{subsystems, SubSystems}|Options]);
    +		      links ->
    +			  SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}])],
    +			  ssh:daemon(0, [{subsystems, SubSystems}|Options]);
    +		      links_root ->
    +			  RootDir = filename:join(PrivDir, links_root),
    +			  ok = file:make_dir(RootDir),
    +			  SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, RootDir}])],
    +			  ssh:daemon(0, [{subsystems, SubSystems}|Options]);
     		      _ ->
     			  SubSystems = [ssh_sftpd:subsystem_spec([])],
     			  ssh:daemon(0, [{subsystems, SubSystems}|Options])
    @@ -468,28 +478,108 @@ real_path(Config) when is_list(Config) ->
     %%--------------------------------------------------------------------
     links(Config) when is_list(Config) ->
         case os:type() of
    -	{win32, _} ->
    -	    {skip, "Links are not fully supported by windows"};
    -	_ ->
    -	    ReqId = 0,
    -	    {Cm, Channel} = proplists:get_value(sftp, Config),
    -	    PrivDir =  proplists:get_value(priv_dir, Config),
    -	    FileName = filename:join(PrivDir, "test.txt"),
    -	    LinkFileName = filename:join(PrivDir, "link_test.txt"),
    +        {win32, _} ->
    +            {skip, "Links are not fully supported by windows"};
    +        _ ->
    +            Sftp = proplists:get_value(sftp, Config),
    +            PrivDir = string:trim(proplists:get_value(priv_dir, Config), trailing, "/"),
    +            LinkPath = filename:join(".", "link"),
     
    -	    {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId),
    -		  ?UINT32(?SSH_FX_OK), _/binary>>, _} =
    -		create_link(LinkFileName, FileName, Cm, Channel, ReqId),
    +            AbsBelowCwd = filename:join(PrivDir, "file"),
    +            links_helper(Sftp, PrivDir, 0, LinkPath, AbsBelowCwd, AbsBelowCwd, AbsBelowCwd, AbsBelowCwd),
     
    -	    NewReqId = 1,
    -	    {ok, <<?SSH_FXP_NAME, ?UINT32(NewReqId), ?UINT32(_), ?UINT32(Len),
    -		  Path:Len/binary, _/binary>>, _}
    -		= read_link(LinkFileName, Cm, Channel, NewReqId),
    +            AbsAtCwd = PrivDir,
    +            links_helper(Sftp, PrivDir, 1, LinkPath, AbsAtCwd, AbsAtCwd, AbsAtCwd, AbsAtCwd),
    +
    +            AbsAboveCwd = filename:join([PrivDir, "..", "file"]),
    +            AbsAboveCwdExpected = filename:join(filename:dirname(PrivDir), "file"),
    +            links_helper(Sftp, PrivDir, 2, LinkPath, AbsAboveCwd, AbsAboveCwdExpected, AbsAboveCwd, AbsAboveCwdExpected),
    +
    +            RelBelowCwd = filename:join(".", "file"),
    +            RelBelowCwdExpected = AbsBelowCwd,
    +            links_helper(Sftp, PrivDir, 3, LinkPath, RelBelowCwd, RelBelowCwdExpected, RelBelowCwd, RelBelowCwdExpected),
    +
    +            RelAtCwd = ".",
    +            RelAtCwdExpected = AbsAtCwd,
    +            links_helper(Sftp, PrivDir, 4, LinkPath, RelAtCwd, RelAtCwdExpected, RelAtCwd, RelAtCwdExpected),
     
    +            RelAboveCwd = filename:join("..", "file"),
    +            RelAboveCwdExpected = AbsAboveCwdExpected,
    +            links_helper(Sftp, PrivDir, 5, LinkPath, RelAboveCwd, RelAboveCwdExpected, RelAboveCwd, RelAboveCwdExpected)
    +    end.
     
    -	    true = binary_to_list(Path) == FileName,
    +%%--------------------------------------------------------------------
    +links_root(Config) when is_list(Config) ->
    +    case os:type() of
    +        {win32, _} ->
    +            {skip, "Links are not fully supported by windows"};
    +        _ ->
    +            Sftp = proplists:get_value(sftp, Config),
    +            PrivDir = proplists:get_value(priv_dir, Config),
    +            Root = filename:join(PrivDir, links_root),
    +            LinkPath = filename:join("/", "link"),
    +
    +            AbsBelowRoot = filename:join(Root, "file"),
    +            AbsBelowRootClient = filename:join("/", "file"),
    +            links_helper(Sftp, Root, 0, LinkPath, AbsBelowRoot, AbsBelowRoot, AbsBelowRootClient, AbsBelowRootClient),
    +
    +            AbsAtRoot = Root,
    +            AbsAtRootExpected = AbsAtRoot,
    +            links_helper(Sftp, Root, 1, LinkPath, AbsAtRoot, AbsAtRootExpected, "/", "/"),
    +
    +            AbsAboveRoot = filename:join(PrivDir, "file"),
    +            AbsAboveRootExpected = AbsAtRootExpected,
    +            AbsAboveRootClient = filename:join("/", ".."),
    +            links_helper(Sftp, Root, 2, LinkPath, AbsAboveRoot, AbsAboveRootExpected, AbsAboveRootClient, "/"),
    +
    +            RelBelowRoot = filename:join(".", "file"),
    +            RelBelowRootExpected = filename:join(Root, "file"),
    +            RelBelowRootClient = RelBelowRoot,
    +            RelBelowRootClientExpected = AbsBelowRootClient,
    +            links_helper(Sftp, Root, 3, LinkPath, RelBelowRoot, RelBelowRootExpected, RelBelowRootClient, RelBelowRootClientExpected),
    +
    +            RelAtRoot = ".",
    +            RelAtRootExpected = Root,
    +            links_helper(Sftp, Root, 4, LinkPath, RelAtRoot, RelAtRootExpected, ".", "/"),
    +
    +            RelAboveRoot = "../file",
    +            RelAboveRootExpected = Root,
    +            RelAboveRootClient = filename:join("..", "file"),
    +            links_helper(Sftp, Root, 5, LinkPath, RelAboveRoot, RelAboveRootExpected, RelAboveRootClient, "/")
    +    end.
     
    -	    ct:log("Path: ~p~n", [binary_to_list(Path)])
    +links_helper({Cm, Channel}, Root, ReqId0, LinkPath, RawTarget, RawExpected, ClientTarget, ClientExpected) ->
    +    ?CT_LOG("RawTarget: ~p, RawExpected: ~p~nClientTarget: ~p, ClientExpected: ~p~n",
    +            [RawTarget, RawExpected, ClientTarget, ClientExpected]),
    +
    +    ReqId1 = ReqId0 * 3,
    +    LinkLocation = filename:join(Root, "link"),
    +    ok = file:make_symlink(RawTarget, LinkLocation),
    +    try
    +        {ok, <<?SSH_FXP_NAME, ?UINT32(ReqId1), ?UINT32(_),
    +               ?UINT32(Len1), ClientActualB1:Len1/binary, _/binary>>, _} =
    +            read_link(LinkPath, Cm, Channel, ReqId1),
    +        ClientActual1 = binary_to_list(ClientActualB1),
    +        ClientExpected = ClientActual1,
    +
    +        ok = file:delete(LinkLocation),
    +
    +        ReqId2 = ReqId1 + 1,
    +        {ok, <<?SSH_FXP_STATUS, ?UINT32(ReqId2),
    +               ?UINT32(?SSH_FX_OK), _/binary>>, _} =
    +            create_link(LinkPath, ClientTarget, Cm, Channel, ReqId2),
    +
    +        {ok, RawActual} = file:read_link(LinkLocation),
    +        RawExpected = string:trim(RawActual, trailing, "/"),
    +
    +        ReqId3 = ReqId2 + 1,
    +        {ok, <<?SSH_FXP_NAME, ?UINT32(ReqId3), ?UINT32(_),
    +               ?UINT32(Len3), ClientActualB2:Len3/binary, _/binary>>, _} =
    +            read_link(LinkPath, Cm, Channel, ReqId3),
    +        ClientActual2 = binary_to_list(ClientActualB2),
    +        ClientExpected = ClientActual2
    +    after
    +        file:delete(LinkLocation)
         end.
     
     %%--------------------------------------------------------------------
    @@ -806,7 +896,9 @@ prep(Config) ->
         %% Initial config
         DataDir = proplists:get_value(data_dir, Config),
         FileName = filename:join(DataDir, "test.txt"),
    -    file:copy(FileName, TestFile),
    +    {ok, Data0} = file:read_file(FileName),
    +    Data = ssh_test_lib:remove_comment(Data0),
    +    ok = file:write_file(TestFile, string:chomp(Data)),
         Mode = 8#00400 bor 8#00200 bor 8#00040, % read & write owner, read group
         {ok, FileInfo} = file:read_file_info(TestFile),
         ok = file:write_file_info(TestFile,
    

Vulnerability mechanics

Root cause

"The SSH_FXP_READLINK handler in ssh_sftpd does not properly sanitize symlink targets, leading to an absolute path disclosure."

Attack vector

An authenticated SFTP client can create a symbolic link within the chroot directory. When the client then requests to read this symbolic link using the SSH_FXP_READLINK handler, the server resolves the link's target to its absolute path on the backend filesystem and returns it to the client. This bypasses the intended chroot isolation for symlink targets [ref_id=1].

Affected code

The vulnerability resides within the SSH_FXP_READLINK handler in the ssh_sftpd module, specifically in the file lib/ssh/src/ssh_sftpd.erl. The commit associated with the fix, 8f4224a0d2676b0653d2c71a889a956e8c2c62d6, indicates changes related to link handling within this module [ref_id=1].

What the fix does

The patch modifies the ssh_sftpd module to ensure that the target of a symbolic link is processed correctly when read via SSH_FXP_READLINK. Specifically, it now calls chroot_filename/2 to strip the backend root prefix from the resolved symlink target before sending it to the client, preventing the disclosure of absolute paths [patch_id=5502042].

Preconditions

  • authThe attacker must be an authenticated SFTP client.
  • inputThe attacker must be able to create a symbolic link within the SFTP chroot directory.

Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

1