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