github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/usersession/userd/launcher.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package userd
    21  
    22  import (
    23  	"fmt"
    24  	"net/url"
    25  	"os"
    26  	"os/exec"
    27  	"syscall"
    28  	"time"
    29  
    30  	"github.com/godbus/dbus"
    31  
    32  	"github.com/snapcore/snapd/i18n"
    33  	"github.com/snapcore/snapd/osutil/sys"
    34  	"github.com/snapcore/snapd/strutil"
    35  	"github.com/snapcore/snapd/usersession/userd/ui"
    36  )
    37  
    38  const launcherIntrospectionXML = `
    39  <interface name="org.freedesktop.DBus.Peer">
    40  	<method name='Ping'>
    41  	</method>
    42  	<method name='GetMachineId'>
    43                 <arg type='s' name='machine_uuid' direction='out'/>
    44  	</method>
    45  </interface>
    46  <interface name='io.snapcraft.Launcher'>
    47  	<method name='OpenURL'>
    48  		<arg type='s' name='url' direction='in'/>
    49  	</method>
    50  	<method name="OpenFile">
    51  		<arg type="s" name="parent_window" direction="in"/>
    52  		<arg type="h" name="fd" direction="in"/>
    53  	</method>
    54  </interface>`
    55  
    56  // allowedURLSchemes are those that can be passed to xdg-open so that it may
    57  // launch the handler for the url scheme on behalf of the snap (and therefore
    58  // outside of the calling snap's confinement). Historically we've been
    59  // conservative about adding url schemes but the thinking was refined in
    60  // https://github.com/snapcore/snapd/pull/7731#pullrequestreview-362900171
    61  //
    62  // The current criteria for adding url schemes is:
    63  // * understanding and documenting the scheme in this file
    64  // * the scheme itself does not cause xdg-open to open files (eg, file:// or
    65  //   matching '^[[:alpha:]+\.\-]+:' (from xdg-open source))
    66  // * verifying that the recipient of the url (ie, what xdg-open calls) won't
    67  //   process file paths/etc that can be leveraged to break out of the sandbox
    68  //   (but understanding how the url can drive the recipient application is
    69  //   important)
    70  //
    71  // This code uses golang's net/url.Parse() which will help ensure the url is
    72  // ok before passing to xdg-open. xdg-open itself properly quotes the url so
    73  // shell metacharacters are blocked.
    74  var (
    75  	allowedURLSchemes = []string{
    76  		// apt: the scheme allows specifying a package for xdg-open to pass to an
    77  		//   apt-handling application, like gnome-software, apturl, etc which are all
    78  		//   protected by policykit
    79  		//   - scheme: apt:<name of package>
    80  		//   - https://github.com/snapcore/snapd/pull/7731
    81  		"apt",
    82  		// help: the scheme allows for specifying a help URL. This code ensures that
    83  		//   the url is parseable
    84  		//   - scheme: help://topic
    85  		//   - https://github.com/snapcore/snapd/pull/6493
    86  		"help",
    87  		// http/https: the scheme allows specifying a web URL. This code ensures that
    88  		//   the url is parseable
    89  		//   - scheme: http(s)://example.com
    90  		"http",
    91  		"https",
    92  		// mailto: the scheme allows for specifying an email address
    93  		//   - scheme: mailto:foo@example.com
    94  		"mailto",
    95  		// msteams: the scheme is a thin wrapper around https.
    96  		//   - scheme: msteams:...
    97  		//   - https://github.com/snapcore/snapd/pull/8761
    98  		"msteams",
    99  		// TODO: document slack URL scheme.
   100  		"slack",
   101  		// snap: the scheme allows specifying a package for xdg-open to pass to a
   102  		//   snap-handling installer application, like snap-store, etc which are
   103  		//   protected by policykit/snap login
   104  		//   - https://github.com/snapcore/snapd/pull/5181
   105  		"snap",
   106  		// zoommtg: the scheme is a modified web url scheme
   107  		//   - scheme: https://medium.com/zoom-developer-blog/zoom-url-schemes-748b95fd9205
   108  		//     (eg, zoommtg://zoom.us/...)
   109  		//   - https://github.com/snapcore/snapd/pull/8304
   110  		"zoommtg",
   111  		// zoomphonecall: another zoom URL scheme, for dialing phone numbers
   112  		//   - https://github.com/snapcore/snapd/pull/8910
   113  		"zoomphonecall",
   114  		// zoomus: alternative name for zoommtg
   115  		//   - https://github.com/snapcore/snapd/pull/8910
   116  		"zoomus",
   117  	}
   118  )
   119  
   120  // Launcher implements the 'io.snapcraft.Launcher' DBus interface.
   121  type Launcher struct {
   122  	conn *dbus.Conn
   123  }
   124  
   125  // Interface returns the name of the interface this object implements
   126  func (s *Launcher) Interface() string {
   127  	return "io.snapcraft.Launcher"
   128  }
   129  
   130  // ObjectPath returns the path that the object is exported as
   131  func (s *Launcher) ObjectPath() dbus.ObjectPath {
   132  	return "/io/snapcraft/Launcher"
   133  }
   134  
   135  // IntrospectionData gives the XML formatted introspection description
   136  // of the DBus service.
   137  func (s *Launcher) IntrospectionData() string {
   138  	return launcherIntrospectionXML
   139  }
   140  
   141  func makeAccessDeniedError(err error) *dbus.Error {
   142  	return &dbus.Error{
   143  		Name: "org.freedesktop.DBus.Error.AccessDenied",
   144  		Body: []interface{}{err.Error()},
   145  	}
   146  }
   147  
   148  // OpenURL implements the 'OpenURL' method of the 'io.snapcraft.Launcher'
   149  // DBus interface. Before the provided url is passed to xdg-open the scheme is
   150  // validated against a list of allowed schemes. All other schemes are denied.
   151  func (s *Launcher) OpenURL(addr string, sender dbus.Sender) *dbus.Error {
   152  	u, err := url.Parse(addr)
   153  	if err != nil {
   154  		return &dbus.ErrMsgInvalidArg
   155  	}
   156  
   157  	if !strutil.ListContains(allowedURLSchemes, u.Scheme) {
   158  		return makeAccessDeniedError(fmt.Errorf("Supplied URL scheme %q is not allowed", u.Scheme))
   159  	}
   160  
   161  	if err := exec.Command("xdg-open", addr).Run(); err != nil {
   162  		return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL"))
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  // fdToFilename determines the path associated with an open file descriptor.
   169  //
   170  // The file descriptor cannot be opened using O_PATH and must refer to
   171  // a regular file or to a directory. The symlink at /proc/self/fd/<fd>
   172  // is read to determine the filename. The descriptor is also fstat'ed
   173  // and the resulting device number and inode number are compared to
   174  // stat on the path determined earlier. The numbers must match.
   175  func fdToFilename(fd int) (string, error) {
   176  	flags, err := sys.FcntlGetFl(fd)
   177  	if err != nil {
   178  		return "", err
   179  	}
   180  	// File descriptors opened with O_PATH do not imply access to
   181  	// the file in question.
   182  	if flags&sys.O_PATH != 0 {
   183  		return "", fmt.Errorf("cannot use file descriptors opened using O_PATH")
   184  	}
   185  
   186  	// Determine the file name associated with the passed file descriptor.
   187  	filename, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd))
   188  	if err != nil {
   189  		return "", err
   190  	}
   191  
   192  	var fileStat, fdStat syscall.Stat_t
   193  	if err := syscall.Stat(filename, &fileStat); err != nil {
   194  		return "", err
   195  	}
   196  	if err := syscall.Fstat(fd, &fdStat); err != nil {
   197  		return "", err
   198  	}
   199  
   200  	// Sanity check to ensure we've got the right file
   201  	if fdStat.Dev != fileStat.Dev || fdStat.Ino != fileStat.Ino {
   202  		return "", fmt.Errorf("cannot determine file name")
   203  	}
   204  
   205  	fileType := fileStat.Mode & syscall.S_IFMT
   206  	if fileType != syscall.S_IFREG && fileType != syscall.S_IFDIR {
   207  		return "", fmt.Errorf("cannot open anything other than regular files or directories")
   208  	}
   209  
   210  	return filename, nil
   211  }
   212  
   213  func (s *Launcher) OpenFile(parentWindow string, clientFd dbus.UnixFD, sender dbus.Sender) *dbus.Error {
   214  	// godbus transfers ownership of this file descriptor to us
   215  	fd := int(clientFd)
   216  	defer syscall.Close(fd)
   217  
   218  	filename, err := fdToFilename(fd)
   219  	if err != nil {
   220  		return dbus.MakeFailedError(err)
   221  	}
   222  
   223  	snap, err := snapFromSender(s.conn, sender)
   224  	if err != nil {
   225  		return dbus.MakeFailedError(err)
   226  	}
   227  	dialog, err := ui.New()
   228  	if err != nil {
   229  		return dbus.MakeFailedError(err)
   230  	}
   231  	answeredYes := dialog.YesNo(
   232  		i18n.G("Allow opening file?"),
   233  		fmt.Sprintf(i18n.G("Allow snap %q to open file %q?"), snap, filename),
   234  		&ui.DialogOptions{
   235  			Timeout: 5 * 60 * time.Second,
   236  			Footer:  i18n.G("This dialog will close automatically after 5 minutes of inactivity."),
   237  		},
   238  	)
   239  	if !answeredYes {
   240  		return dbus.MakeFailedError(fmt.Errorf("permission denied"))
   241  	}
   242  
   243  	if err = exec.Command("xdg-open", filename).Run(); err != nil {
   244  		return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL"))
   245  	}
   246  
   247  	return nil
   248  }