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 }