zserge / lorca

Build cross-platform modern desktop apps in Go + HTML5
MIT License
8k stars 532 forks source link

security: Chrome debugging protocol is open to anyone on localhost #43

Open tv42 opened 5 years ago

tv42 commented 5 years ago

Hi. Lorca currently exposes all information about the UI to all processes on localhost, and allows any local process to hijack the UI.

https://peter.sh/experiments/chromium-command-line-switches/#remote-debugging-address

$ mkdir z
$ cd z
$ go mod init demo
go: creating new go.mod: module demo
$ curl https://raw.githubusercontent.com/zserge/lorca/master/examples/hello/main.go >main.go
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   408  100   408    0     0    542      0 --:--:-- --:--:-- --:--:--   541
$ go run main.go
go: finding github.com/zserge/lorca v0.1.6
go: downloading github.com/zserge/lorca v0.1.6
go: extracting github.com/zserge/lorca v0.1.6

As the attacker:

$ ss -ltnp |grep chrome
LISTEN   0         10                127.0.0.1:42381            0.0.0.0:*        users:(("chrome",pid=1703,fd=98))                                              
$ google-chrome http://127.0.0.1:42381
# observe the UI contents happily

I would recommend you switch to --remote-debugging-pipe: https://github.com/chromium/chromium/blob/d925e7f357d93b95631c22c47e2cc93b093dacc5/content/public/common/content_switches.cc#L700-L703

zserge commented 5 years ago

@tv42 Totally support your idea, and this was the original intention. However, pipes are not well supported on Windows (at least in Go). So if you have an idea how it can be implemented on Windows without much cgo hassle - please let me know!

tv42 commented 5 years ago

Oh, funny, because I understood the motivation for implementing the pipe functionality in Chrome was that the original FD passing mechanism wasn't good for Windows.

My understanding is that os.Pipe and os/exec.Cmd.ExtraFiles work on Windows just like they would on anything else (click those links to see the implementation), but I don't "do Windows" so I'm not the right person to ask.

Even if this was something where Windows was not capable of doing a thing, the current mechanism essentially makes Lorca unusable for anything beyond a demo. Really.

zserge commented 5 years ago

@tv42 Actually, in the exec.Cmd.ExtraFiles comment it states: // ExtraFiles is not supported on Windows. and it seems to be true.

tv42 commented 5 years ago

Oh. Funky. Sorry for missing that. This seems to be the relevant Go issue: https://github.com/golang/go/issues/21085

Based on the that issue, it sounds like this restriction in Go comes from Windows file handles not being sequentially numbered, but then the Chrome docs talking about fd 3/4 get confusing. Plus, their issue tracking implies -pipe won over -fd because of Windows support ("I don't think you can wrap single pipe with TCP socket in Windows" is the argument against using a single fd in https://chromium-review.googlesource.com/c/chromium/src/+/954405/ ). I added a comment to the Go issue in hopes that someone will know if these puzzle pieces connect.

Chasing down what happens on Windows, links to interesting bits in source/docs:

https://cs.chromium.org/chromium/src/content/browser/devtools/devtools_pipe_handler.cc?q=read_handle_&l=49&dr=C https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/get-osfhandle?view=vs-2017

Here's evidence of things using fds 3/4 happily, with further resources in comments. Puppeteer especially is specially advertised as working on Windows: https://pptr.dev

https://github.com/GoogleChrome/puppeteer/issues/2079#issuecomment-373568350 https://github.com/GoogleChromeLabs/carlo/issues/11 https://github.com/cyrus-and/chrome-remote-interface/issues/381

Srinivasa314 commented 4 years ago

I managed to use pipes in my library https://github.com/Srinivasa314/alcro in the master branch, but you need to use cgo for it. Also headless mode does not work (I dont know why)

tv42 commented 2 years ago

For what it's worth, https://github.com/golang/go/issues/21085 is now closed, and thus this should be fixable?

rtpt-alexanderneumann commented 2 years ago

FWIW, I've implemented the pipes solution for Linux (and it works great), please let me know if that's something you'd like to add (even if it does not implement it on Windows).

I've split out the websocket connection, built a Channel interface, and here's how to connect to Chrome via pipes:

package lorca

import (
    "bufio"
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "os"
    "os/exec"
)

// Channel exchanges messages with the running browser instance.
type Channel interface {
    Send(interface{}) error
    Receive(interface{}) error
    Close() error
}

type pipeChannel struct {
    pout io.WriteCloser
    pin  io.ReadCloser
    rd   *bufio.Reader
}

func runChromeWithPipes(chromeBinary string, args ...string) (*exec.Cmd, Channel, error) {
    rIn, wIn, err := os.Pipe()
    if err != nil {
        return nil, nil, fmt.Errorf("create pipe failed: %w", err)
    }

    rOut, wOut, err := os.Pipe()
    if err != nil {
        return nil, nil, fmt.Errorf("create pipe failed: %w", err)
    }

    // Start chrome process
        args = append(args, "--remote-debugging-pipe")
    cmd := exec.Command(chromeBinary, args...)
    cmd.ExtraFiles = []*os.File{rIn, wOut}

    if err != nil {
        return nil, nil, err
    }

    if err := cmd.Start(); err != nil {
        return nil, nil, err
    }

    ch := &pipeChannel{
        pout: wIn,
        pin:  rOut,
        rd:   bufio.NewReader(rOut),
    }
    return cmd, ch, nil
}

func (ch *pipeChannel) Close() error {
    err := ch.pout.Close()
    errOut := ch.pin.Close()

    if err == nil {
        err = errOut
    }

    if err != nil {
        return fmt.Errorf("close pipe channel failed: %w", err)
    }

    return nil
}

func (ch *pipeChannel) Send(obj interface{}) error {
    buf, err := json.Marshal(obj)
    if err != nil {
        return fmt.Errorf("json encode failed: %w", err)
    }

    // terminate the message with a null byte
    buf = append(buf, 0)

    _, err = ch.pout.Write(buf)
    if err != nil {
        return fmt.Errorf("write to chrome instance failed: %w", err)
    }

    return nil
}

func (ch *pipeChannel) Receive(obj interface{}) error {
    // read until the next null byte
    buf, err := ch.rd.ReadBytes(0)
    if err != nil {
        return fmt.Errorf("read message from chrome failed: %w", err)
    }

    buf = bytes.TrimRight(buf, "\x00")

    err = json.Unmarshal(buf, obj)
    if err != nil {
        return fmt.Errorf("json unmarshal failed: %w", err)
    }

    return nil
}