CVE-2021-41088 (published 2021-09-23T20:15Z)

Elvish is a programming language and interactive shell, combined into one package. In versions prior to 0.14.0 Elvish's web UI backend (started by `elvish -web`) hosts an endpoint that allows executing the code sent from the web UI. The backend does not check the origin of requests correctly. As a result, if the user has the web UI backend open and visits a compromised or malicious website, the website can send arbitrary code to the endpoint in localhost. All Elvish releases from 0.14.0 onward no longer include the the web UI, although it is still possible for the user to build a version from source that includes the web UI. The issue can be patched for previous versions by removing the web UI (found in web, pkg/web or pkg/prog/web, depending on the exact version).

Severity: HIGH
CVSS v2 base score: 9.3 (AV:N/AC:M/Au:N/C:C/I:C/A:C)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-346
cwe_name: Origin Validation Error
description: The product does not properly verify that the source of data or communication is valid.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/elves/elvish
commit_details: repo_url: https://github.com/elves/elvish
author: Qi Xiao
author_date: 2021-09-21 23:45:09+01:00
msg: Remove the experimental web UI.

It has not been actively developed for a long time and has a vulnerability:
https://github.com/elves/elvish/security/advisories/GHSA-fpv6-f8jw-rc3r
repository: repo_url: https://github.com/elves/elvish
repo_name: elves/elvish
description: Powerful scripting language & versatile interactive shell
repo_language: Go
owner: elves
file_changes: filename: main.go
change_type: ModificationType.DELETE
diff: @@ -1,22 +0,0 @@
-// Command elvish is an alternative main program of Elvish that includes the web
-// subprogram.
-package main
-
-import (
-	"os"
-
-	"src.elv.sh/pkg/buildinfo"
-	"src.elv.sh/pkg/daemon"
-	"src.elv.sh/pkg/daemon/client"
-	"src.elv.sh/pkg/prog"
-	"src.elv.sh/pkg/shell"
-	"src.elv.sh/pkg/web"
-)
-
-func main() {
-	os.Exit(prog.Run(
-		[3]*os.File{os.Stdin, os.Stdout, os.Stderr}, os.Args,
-		prog.Composite(
-			buildinfo.Program, daemon.Program, web.Program,
-			shell.Program{ActivateDaemon: client.Activate})))
-}

programming_language: unknown
filename: main.html.go
change_type: ModificationType.DELETE
diff: @@ -1,233 +0,0 @@
-package web
-
-const mainPageHTML = `<html>
-
-  <body class="light">
-    <div id="content">
-      <div id="scrollback">
-        <div id="progress"></div>
-      </div>
-
-      <div id="cmd" class="cmd">
-        <span id="prompt" class="prompt">&gt;&gt;</span>
-        <textarea id="buffer" class="buffer" rows="1"></textarea>
-      </div>
-
-      <div id="theme-switchers" class="flex">
-        <span class="theme-switcher" id="dark-theme">dark</span>
-        <span class="theme-switcher" id="light-theme">light</span>
-      </div>
-
-    </div>
-  </body>
-
-  <style>
-    /* Colors are taken from Material palette. */
-
-    /* Global styles */
-
-    * {
-      margin: 0;
-      padding: 0;
-      font: 12pt monospace;
-    }
-
-    #content {
-      margin: 20px;
-      padding: 20px;
-    }
-
-    /* Scrollback */
-
-    .exception {
-      font-weight: bold;
-    }
-
-    .server-error {
-      font-style: italic;
-    }
-
-    /* Command line */
-
-    .cmd {
-      display: flex;
-      width: 100%;
-    }
-
-    .cmd > .prompt {
-      display: inline-block;
-      margin-right: 1em;
-    }
-
-    .cmd > .buffer {
-      flex: 1;
-    }
-
-    /* Theme switcher */
-
-    #theme-switchers {
-      display: flex;
-      margin-top: 0.4em;
-    }
-
-    .theme-switcher {
-      cursor: pointer;
-      padding: 0 1em;
-      border: 1px solid;
-    }
-
-    #light-theme {
-      color: black;
-      background-color: white;
-      border-color: black;
-    }
-
-    #dark-theme {
-      color: white;
-      background-color: black;
-      border-color: white;
-    }
-
-    /* Color schemes. Color values from Material palette. */
-
-    body.light {
-      background: #EEEEEE; /* grey 200 */
-    }
-
-    .light * {
-      color: black;
-      background: white;
-    }
-
-    .light #content {
-      background: white;
-    }
-
-    .light .cmd > .prompt {
-      color: #1565C0; /* blue 800 */
-    }
-
-    .light .cmd > #prompt {
-      color: #2E7D32; /* green 800 */
-    }
-
-    .light .error {
-      color: #C62828; /* red 800 */
-    }
-
-
-    .dark * {
-      color: white;
-      background: black;
-    }
-
-    body.dark {
-      background: #212121; /* grey 900 */
-    }
-
-    .dark #content {
-      background: black;
-    }
-
-    .dark .cmd > .prompt {
-      color: #90CAF9; /* blue 200 */
-    }
-
-    .dark .cmd > #prompt {
-      color: #A5D6A7; /* green 200 */
-    }
-
-    .dark .error {
-      color: #EF9A9A; /* red 200 */
-    }
-
-  </style>
-
-  <script>
-    // TODO(xiaq): Stream results.
-    var $prompt = document.getElementById('prompt'),
-        $buffer = document.getElementById('buffer'),
-        $scrollback = document.getElementById('scrollback'),
-        $progress = document.getElementById('progress');
-
-    /* Theme switchers. */
-    document.getElementById('dark-theme').onclick = function() {
-      document.body.className = 'dark';
-    };
-
-    document.getElementById('light-theme').onclick = function() {
-      document.body.className = 'light';
-    };
-
-    $buffer.addEventListener('keypress', function(e) {
-      if (e.keyCode == 13 &&
-          !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
-        e.preventDefault();
-        execute();
-      }
-    });
-
-    function execute() {
-      var code = $buffer.value;
-      addToScrollbackInner(freezeCmd());
-      $buffer.value = '';
-      $progress.innerText = 'executing...';
-
-      var req = new XMLHttpRequest();
-      req.onloadend = function() {
-        $progress.innerText = '';
-      };
-      req.onload = function() {
-        var res = JSON.parse(req.responseText);
-        addToScrollback('output', res.OutBytes);
-        if (res.OutValues) {
-          for (var v of res.OutValues) {
-            addToScrollback('output-value', v);
-          }
-        }
-        addToScrollback('error', res.ErrBytes);
-        addToScrollback('error exception', res.Err);
-      };
-      req.onerror = function() {
-        addToScrollback('error server-error', req.responseText
-          || req.statusText
-          || (req.status == req.UNSENT && "lost connection")
-          || "unknown error");
-      };
-      req.open('POST', '/execute');
-      req.send(code);
-    }
-
-    function addToScrollback(className, innerText) {
-      var div = document.createElement('div');
-      div.className = className;
-      div.innerText = innerText;
-
-      addToScrollbackInner(div);
-    }
-
-    function addToScrollbackInner(element) {
-      $scrollback.insertBefore(element, $progress);
-      window.scrollTo(0, document.body.scrollHeight);
-    }
-
-    function freezeCmd() {
-      var cmd = document.createElement('div'),
-          prompt = document.createElement('span'),
-          buffer = document.createElement('span');
-      cmd.className = 'cmd';
-      prompt.className = 'prompt';
-      prompt.innerText = $prompt.innerText;
-      buffer.className = 'buffer';
-      buffer.innerText = $buffer.value;
-      cmd.appendChild(prompt);
-      cmd.appendChild(buffer);
-      return cmd;
-    }
-
-  </script>
-
-</html>
-`
-
-// vim: se ft=html si et sw=2 ts=2 sts=2:

programming_language: unknown
filename: web.go
change_type: ModificationType.DELETE
diff: @@ -1,183 +0,0 @@
-// Package web is the entry point for the backend of the web interface of
-// Elvish.
-package web
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"log"
-	"net/http"
-	"os"
-
-	"src.elv.sh/pkg/eval"
-	"src.elv.sh/pkg/parse"
-	"src.elv.sh/pkg/prog"
-	"src.elv.sh/pkg/shell"
-)
-
-// Program is the web subprogram.
-var Program prog.Program = program{}
-
-type program struct{}
-
-func (program) Run(fds [3]*os.File, f *prog.Flags, args []string) error {
-	if !f.Web {
-		return prog.ErrNotSuitable
-	}
-	if len(args) > 0 {
-		return prog.BadUsage("arguments are not allowed with -web")
-	}
-	if f.CodeInArg {
-		return prog.BadUsage("-c cannot be used together with -web")
-	}
-	p := Web{Port: f.Port}
-	return p.Main(fds, nil)
-}
-
-type Web struct {
-	Port int
-}
-
-type httpHandler struct {
-	ev *eval.Evaler
-}
-
-type ExecuteResponse struct {
-	OutBytes  string
-	OutValues []interface{}
-	ErrBytes  string
-	Err       string
-}
-
-func (web *Web) Main(fds [3]*os.File, _ []string) error {
-	restore := shell.IncSHLVL()
-	defer restore()
-	ev := shell.MakeEvaler(fds[2])
-
-	h := httpHandler{ev}
-
-	http.HandleFunc("/", h.handleMainPage)
-	http.HandleFunc("/execute", h.handleExecute)
-	addr := fmt.Sprintf("localhost:%d", web.Port)
-	log.Println("going to listen", addr)
-	err := http.ListenAndServe(addr, nil)
-
-	log.Println(err)
-	return nil
-}
-
-func (h httpHandler) handleMainPage(w http.ResponseWriter, r *http.Request) {
-	_, err := w.Write([]byte(mainPageHTML))
-	if err != nil {
-		log.Println("cannot write response:", err)
-	}
-}
-
-func (h httpHandler) handleExecute(w http.ResponseWriter, r *http.Request) {
-	bytes, err := io.ReadAll(r.Body)
-	if err != nil {
-		log.Println("cannot read request body:", err)
-		return
-	}
-	code := string(bytes)
-
-	outBytes, outValues, errBytes, err := evalAndCollect(h.ev, code)
-	errText := ""
-	if err != nil {
-		errText = err.Error()
-	}
-	responseBody, err := json.Marshal(
-		&ExecuteResponse{string(outBytes), outValues, string(errBytes), errText})
-	if err != nil {
-		log.Println("cannot marshal response body:", err)
-	}
-
-	_, err = w.Write(responseBody)
-	if err != nil {
-		log.Println("cannot write response:", err)
-	}
-}
-
-const (
-	outFileBufferSize = 1024
-	outChanBufferSize = 32
-)
-
-// evalAndCollect evaluates a piece of code with null stdin, and stdout and
-// stderr connected to pipes (value part of stderr being a blackhole), and
-// return the results collected on stdout and stderr, and the possible error
-// that occurred.
-func evalAndCollect(ev *eval.Evaler, code string) (
-	outBytes []byte, outValues []interface{}, errBytes []byte, err error) {
-
-	outFile, chanOutBytes := makeBytesWriterAndCollect()
-	outChan, chanOutValues := makeValuesWriterAndCollect()
-	errFile, chanErrBytes := makeBytesWriterAndCollect()
-
-	ports := []*eval.Port{
-		eval.DummyInputPort,
-		{File: outFile, Chan: outChan},
-		{File: errFile, Chan: eval.BlackholeChan},
-	}
-	err = ev.Eval(
-		parse.Source{Name: "[web]", Code: code}, eval.EvalCfg{Ports: ports})
-
-	outFile.Close()
-	close(outChan)
-	errFile.Close()
-	return <-chanOutBytes, <-chanOutValues, <-chanErrBytes, err
-}
-
-// makeBytesWriterAndCollect makes an in-memory file that can be written to, and
-// the written bytes will be collected in a byte slice that will be put on a
-// channel as soon as the writer is closed.
-func makeBytesWriterAndCollect() (*os.File, <-chan []byte) {
-	r, w, err := os.Pipe()
-	// os.Pipe returns error only on resource exhaustion.
-	if err != nil {
-		panic(err)
-	}
-	chanCollected := make(chan []byte)
-
-	go func() {
-		var (
-			collected []byte
-			buf       [outFileBufferSize]byte
-		)
-		for {
-			n, err := r.Read(buf[:])
-			collected = append(collected, buf[:n]...)
-			if err != nil {
-				if err != io.EOF {
-					log.Println("error when reading output pipe:", err)
-				}
-				break
-			}
-		}
-		r.Close()
-		chanCollected <- collected
-	}()
-
-	return w, chanCollected
-}
-
-// makeValuesWriterAndCollect makes a Value channel for writing, and the written
-// values will be collected in a Value slice that will be put on a channel as
-// soon as the writer is closed.
-func makeValuesWriterAndCollect() (chan interface{}, <-chan []interface{}) {
-	chanValues := make(chan interface{}, outChanBufferSize)
-	chanCollected := make(chan []interface{})
-
-	go func() {
-		var collected []interface{}
-		for {
-			for v := range chanValues {
-				collected = append(collected, v)
-			}
-			chanCollected <- collected
-		}
-	}()
-
-	return chanValues, chanCollected
-}

programming_language: unknown
filename: web_test.go
change_type: ModificationType.DELETE
diff: @@ -1,19 +0,0 @@
-package web
-
-import (
-	"testing"
-
-	. "src.elv.sh/pkg/prog/progtest"
-)
-
-func TestProgram(t *testing.T) {
-	Test(t, Program,
-		ThatElvish("-web", "x").
-			ExitsWith(2).
-			WritesStderrContaining("arguments are not allowed with -web"),
-
-		ThatElvish("-web", "-c").
-			ExitsWith(2).
-			WritesStderrContaining("-c cannot be used together with -web"),
-	)
-}

programming_language: unknown