Skip to content

Commit

Permalink
Merge commit from fork
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw authored Feb 8, 2025
1 parent da1de1b commit de85afd
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 85 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

* Restrict access to esbuild's development server ([GHSA-67mh-4wv8-2f99](https://github.com/evanw/esbuild/security/advisories/GHSA-67mh-4wv8-2f99))

This change addresses esbuild's first security vulnerability report. Previously esbuild set the `Access-Control-Allow-Origin` header to `*` to allow esbuild's development server to be flexible in how it's used for development. However, this allows the websites you visit to make HTTP requests to esbuild's local development server, which gives read-only access to your source code if the website were to fetch your source code's specific URL. You can read more information in [the report](https://github.com/evanw/esbuild/security/advisories/GHSA-67mh-4wv8-2f99).

Starting with this release, [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) will now be disabled, and requests will now be denied if the host does not match the one provided to `--serve=`. The default host is `0.0.0.0`, which refers to all of the IP addresses that represent the local machine (e.g. both `127.0.0.1` and `192.168.0.1`). If you want to customize anything about esbuild's development server, you can [put a proxy in front of esbuild](https://esbuild.github.io/api/#serve-proxy) and modify the incoming and/or outgoing requests.

In addition, the `serve()` API call has been changed to return an array of `hosts` instead of a single `host` string. This makes it possible to determine all of the hosts that esbuild's development server will accept.

Thanks to [@sapphi-red](https://github.com/sapphi-red) for reporting this issue.

* Delete output files when a build fails in watch mode ([#3643](https://github.com/evanw/esbuild/issues/3643))

It has been requested for esbuild to delete files when a build fails in watch mode. Previously esbuild left the old files in place, which could cause people to not immediately realize that the most recent build failed. With this release, esbuild will now delete all output files if a rebuild fails. Fixing the build error and triggering another rebuild will restore all output files again.
Expand Down
8 changes: 6 additions & 2 deletions cmd/esbuild/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,15 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) {
if result, err := ctx.Serve(options); err != nil {
service.sendPacket(encodeErrorPacket(p.id, err))
} else {
hosts := make([]interface{}, len(result.Hosts))
for i, host := range result.Hosts {
hosts[i] = host
}
service.sendPacket(encodePacket(packet{
id: p.id,
value: map[string]interface{}{
"port": int(result.Port),
"host": result.Host,
"port": int(result.Port),
"hosts": hosts,
},
}))
}
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/stdio_protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface ServeRequest {

export interface ServeResponse {
port: number
host: string
hosts: string[]
}

export interface BuildPlugin {
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export interface ServeOnRequestArgs {
/** Documentation: https://esbuild.github.io/api/#serve-return-values */
export interface ServeResult {
port: number
host: string
hosts: string[]
}

export interface TransformOptions extends CommonOptions {
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,8 @@ type ServeOnRequestArgs struct {

// Documentation: https://esbuild.github.io/api/#serve-return-values
type ServeResult struct {
Port uint16
Host string
Port uint16
Hosts []string
}

type WatchOptions struct {
Expand Down
77 changes: 49 additions & 28 deletions pkg/api/serve_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type apiHandler struct {
keyfileToLower string
certfileToLower string
fallback string
hosts []string
serveWaitGroup sync.WaitGroup
activeStreams []chan serverSentEvent
currentHashes map[string]string
Expand Down Expand Up @@ -103,22 +104,44 @@ func errorsToString(errors []Message) string {
func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
start := time.Now()

// Special-case the esbuild event stream
if req.Method == "GET" && req.URL.Path == "/esbuild" && req.Header.Get("Accept") == "text/event-stream" {
h.serveEventStream(start, req, res)
return
}

// HEAD requests omit the body
maybeWriteResponseBody := func(bytes []byte) { res.Write(bytes) }
isHEAD := req.Method == "HEAD"
if isHEAD {
maybeWriteResponseBody = func([]byte) { res.Write(nil) }
}

// Check the "Host" header to prevent DNS rebinding attacks
if strings.ContainsRune(req.Host, ':') {
// Try to strip off the port number
if host, _, err := net.SplitHostPort(req.Host); err == nil {
req.Host = host
}
}
if req.Host != "localhost" {
ok := false
for _, allowed := range h.hosts {
if req.Host == allowed {
ok = true
break
}
}
if !ok {
go h.notifyRequest(time.Since(start), req, http.StatusForbidden)
res.WriteHeader(http.StatusForbidden)
maybeWriteResponseBody([]byte(fmt.Sprintf("403 - Forbidden: The host %q is not allowed", req.Host)))
return
}
}

// Special-case the esbuild event stream
if req.Method == "GET" && req.URL.Path == "/esbuild" && req.Header.Get("Accept") == "text/event-stream" {
h.serveEventStream(start, req, res)
return
}

// Handle GET and HEAD requests
if (isHEAD || req.Method == "GET") && strings.HasPrefix(req.URL.Path, "/") {
res.Header().Set("Access-Control-Allow-Origin", "*")
queryPath := path.Clean(req.URL.Path)[1:]
result := h.rebuild()

Expand Down Expand Up @@ -360,7 +383,6 @@ func (h *apiHandler) serveEventStream(start time.Time, req *http.Request, res ht
res.Header().Set("Content-Type", "text/event-stream")
res.Header().Set("Connection", "keep-alive")
res.Header().Set("Cache-Control", "no-cache")
res.Header().Set("Access-Control-Allow-Origin", "*")
go h.notifyRequest(time.Since(start), req, http.StatusOK)
res.WriteHeader(http.StatusOK)
res.Write([]byte("retry: 500\n"))
Expand Down Expand Up @@ -789,11 +811,26 @@ func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error

// Extract the real port in case we passed a port of "0"
var result ServeResult
var boundHost string
if host, text, err := net.SplitHostPort(addr); err == nil {
if port, err := strconv.ParseInt(text, 10, 32); err == nil {
result.Port = uint16(port)
result.Host = host
boundHost = host
}
}

// Build up a list of all hosts we use
if ip := net.ParseIP(boundHost); ip != nil && ip.IsUnspecified() {
// If this is "0.0.0.0" or "::", list all relevant IP addresses
if addrs, err := net.InterfaceAddrs(); err == nil {
for _, addr := range addrs {
if addr, ok := addr.(*net.IPNet); ok && (addr.IP.To4() != nil) == (ip.To4() != nil) && !addr.IP.IsLinkLocalUnicast() {
result.Hosts = append(result.Hosts, addr.IP.String())
}
}
}
} else {
result.Hosts = append(result.Hosts, boundHost)
}

// HTTPS-related files should be absolute paths
Expand All @@ -815,6 +852,7 @@ func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error
keyfileToLower: strings.ToLower(serveOptions.Keyfile),
certfileToLower: strings.ToLower(serveOptions.Certfile),
fallback: serveOptions.Fallback,
hosts: append([]string{}, result.Hosts...),
rebuild: func() BuildResult {
if atomic.LoadInt32(&shouldStop) != 0 {
// Don't start more rebuilds if we were told to stop
Expand Down Expand Up @@ -905,7 +943,7 @@ func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error

// Print the URL(s) that the server can be reached at
if ctx.args.logOptions.LogLevel <= logger.LevelInfo {
printURLs(result.Host, result.Port, isHTTPS, ctx.args.logOptions.Color)
printURLs(handler.hosts, result.Port, isHTTPS, ctx.args.logOptions.Color)
}

// Start the first build shortly after this function returns (but not
Expand Down Expand Up @@ -941,28 +979,11 @@ func (hack *hackListener) Accept() (net.Conn, error) {
return hack.Listener.Accept()
}

func printURLs(host string, port uint16, https bool, useColor logger.UseColor) {
func printURLs(hosts []string, port uint16, https bool, useColor logger.UseColor) {
logger.PrintTextWithColor(os.Stderr, useColor, func(colors logger.Colors) string {
var hosts []string
sb := strings.Builder{}
sb.WriteString(colors.Reset)

// If this is "0.0.0.0" or "::", list all relevant IP addresses
if ip := net.ParseIP(host); ip != nil && ip.IsUnspecified() {
if addrs, err := net.InterfaceAddrs(); err == nil {
for _, addr := range addrs {
if addr, ok := addr.(*net.IPNet); ok && (addr.IP.To4() != nil) == (ip.To4() != nil) && !addr.IP.IsLinkLocalUnicast() {
hosts = append(hosts, addr.IP.String())
}
}
}
}

// Otherwise, just list the one IP address
if len(hosts) == 0 {
hosts = append(hosts, host)
}

// Determine the host kinds
kinds := make([]string, len(hosts))
maxLen := 0
Expand Down
Loading

0 comments on commit de85afd

Please sign in to comment.