Skip to content

Commit de85afd

Browse files
authoredFeb 8, 2025··
Merge commit from fork
1 parent da1de1b commit de85afd

File tree

7 files changed

+249
-85
lines changed

7 files changed

+249
-85
lines changed
 

‎CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## Unreleased
44

5+
* Restrict access to esbuild's development server ([GHSA-67mh-4wv8-2f99](https://github.com/evanw/esbuild/security/advisories/GHSA-67mh-4wv8-2f99))
6+
7+
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).
8+
9+
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.
10+
11+
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.
12+
13+
Thanks to [@sapphi-red](https://github.com/sapphi-red) for reporting this issue.
14+
515
* Delete output files when a build fails in watch mode ([#3643](https://github.com/evanw/esbuild/issues/3643))
616

717
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.

‎cmd/esbuild/service.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,15 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) {
424424
if result, err := ctx.Serve(options); err != nil {
425425
service.sendPacket(encodeErrorPacket(p.id, err))
426426
} else {
427+
hosts := make([]interface{}, len(result.Hosts))
428+
for i, host := range result.Hosts {
429+
hosts[i] = host
430+
}
427431
service.sendPacket(encodePacket(packet{
428432
id: p.id,
429433
value: map[string]interface{}{
430-
"port": int(result.Port),
431-
"host": result.Host,
434+
"port": int(result.Port),
435+
"hosts": hosts,
432436
},
433437
}))
434438
}

‎lib/shared/stdio_protocol.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface ServeRequest {
3535

3636
export interface ServeResponse {
3737
port: number
38-
host: string
38+
hosts: string[]
3939
}
4040

4141
export interface BuildPlugin {

‎lib/shared/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export interface ServeOnRequestArgs {
256256
/** Documentation: https://esbuild.github.io/api/#serve-return-values */
257257
export interface ServeResult {
258258
port: number
259-
host: string
259+
hosts: string[]
260260
}
261261

262262
export interface TransformOptions extends CommonOptions {

‎pkg/api/api.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,8 @@ type ServeOnRequestArgs struct {
491491

492492
// Documentation: https://esbuild.github.io/api/#serve-return-values
493493
type ServeResult struct {
494-
Port uint16
495-
Host string
494+
Port uint16
495+
Hosts []string
496496
}
497497

498498
type WatchOptions struct {

‎pkg/api/serve_other.go

+49-28
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type apiHandler struct {
4848
keyfileToLower string
4949
certfileToLower string
5050
fallback string
51+
hosts []string
5152
serveWaitGroup sync.WaitGroup
5253
activeStreams []chan serverSentEvent
5354
currentHashes map[string]string
@@ -103,22 +104,44 @@ func errorsToString(errors []Message) string {
103104
func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
104105
start := time.Now()
105106

106-
// Special-case the esbuild event stream
107-
if req.Method == "GET" && req.URL.Path == "/esbuild" && req.Header.Get("Accept") == "text/event-stream" {
108-
h.serveEventStream(start, req, res)
109-
return
110-
}
111-
112107
// HEAD requests omit the body
113108
maybeWriteResponseBody := func(bytes []byte) { res.Write(bytes) }
114109
isHEAD := req.Method == "HEAD"
115110
if isHEAD {
116111
maybeWriteResponseBody = func([]byte) { res.Write(nil) }
117112
}
118113

114+
// Check the "Host" header to prevent DNS rebinding attacks
115+
if strings.ContainsRune(req.Host, ':') {
116+
// Try to strip off the port number
117+
if host, _, err := net.SplitHostPort(req.Host); err == nil {
118+
req.Host = host
119+
}
120+
}
121+
if req.Host != "localhost" {
122+
ok := false
123+
for _, allowed := range h.hosts {
124+
if req.Host == allowed {
125+
ok = true
126+
break
127+
}
128+
}
129+
if !ok {
130+
go h.notifyRequest(time.Since(start), req, http.StatusForbidden)
131+
res.WriteHeader(http.StatusForbidden)
132+
maybeWriteResponseBody([]byte(fmt.Sprintf("403 - Forbidden: The host %q is not allowed", req.Host)))
133+
return
134+
}
135+
}
136+
137+
// Special-case the esbuild event stream
138+
if req.Method == "GET" && req.URL.Path == "/esbuild" && req.Header.Get("Accept") == "text/event-stream" {
139+
h.serveEventStream(start, req, res)
140+
return
141+
}
142+
119143
// Handle GET and HEAD requests
120144
if (isHEAD || req.Method == "GET") && strings.HasPrefix(req.URL.Path, "/") {
121-
res.Header().Set("Access-Control-Allow-Origin", "*")
122145
queryPath := path.Clean(req.URL.Path)[1:]
123146
result := h.rebuild()
124147

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

790812
// Extract the real port in case we passed a port of "0"
791813
var result ServeResult
814+
var boundHost string
792815
if host, text, err := net.SplitHostPort(addr); err == nil {
793816
if port, err := strconv.ParseInt(text, 10, 32); err == nil {
794817
result.Port = uint16(port)
795-
result.Host = host
818+
boundHost = host
819+
}
820+
}
821+
822+
// Build up a list of all hosts we use
823+
if ip := net.ParseIP(boundHost); ip != nil && ip.IsUnspecified() {
824+
// If this is "0.0.0.0" or "::", list all relevant IP addresses
825+
if addrs, err := net.InterfaceAddrs(); err == nil {
826+
for _, addr := range addrs {
827+
if addr, ok := addr.(*net.IPNet); ok && (addr.IP.To4() != nil) == (ip.To4() != nil) && !addr.IP.IsLinkLocalUnicast() {
828+
result.Hosts = append(result.Hosts, addr.IP.String())
829+
}
830+
}
796831
}
832+
} else {
833+
result.Hosts = append(result.Hosts, boundHost)
797834
}
798835

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

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

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

944-
func printURLs(host string, port uint16, https bool, useColor logger.UseColor) {
982+
func printURLs(hosts []string, port uint16, https bool, useColor logger.UseColor) {
945983
logger.PrintTextWithColor(os.Stderr, useColor, func(colors logger.Colors) string {
946-
var hosts []string
947984
sb := strings.Builder{}
948985
sb.WriteString(colors.Reset)
949986

950-
// If this is "0.0.0.0" or "::", list all relevant IP addresses
951-
if ip := net.ParseIP(host); ip != nil && ip.IsUnspecified() {
952-
if addrs, err := net.InterfaceAddrs(); err == nil {
953-
for _, addr := range addrs {
954-
if addr, ok := addr.(*net.IPNet); ok && (addr.IP.To4() != nil) == (ip.To4() != nil) && !addr.IP.IsLinkLocalUnicast() {
955-
hosts = append(hosts, addr.IP.String())
956-
}
957-
}
958-
}
959-
}
960-
961-
// Otherwise, just list the one IP address
962-
if len(hosts) == 0 {
963-
hosts = append(hosts, host)
964-
}
965-
966987
// Determine the host kinds
967988
kinds := make([]string, len(hosts))
968989
maxLen := 0

‎scripts/js-api-tests.js

+180-51
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.