package console // Verifying an untrusted GeoIP database in a child process (code review // R9). A .mmdb from a feed or an upload is parsed by maxminddb, which // trusts its metadata: a crafted file can panic it (node_count arithmetic) // or make its tree walk run for 2^128 steps inside a single call. Neither // may take the console down or hold the publish slot past its deadline, so // the console never parses such a file itself: it runs its own binary with // a hidden verb, kills it at the deadline, and reads back a small report. import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "os" "os/exec" "time" "github.com/carlosframework/platform/internal/geo" ) // GeoipVerifyVerb is cmd/console's hidden verb for the verifier child. const GeoipVerifyVerb = "__geoip-verify" // geoipVerifyResult is the child's report. type geoipVerifyResult struct { Source string `json:"source"` BuildEpoch int64 `json:"build_epoch"` NodeCount uint64 `json:"node_count"` Countries int `json:"countries"` // how many probe addresses answered a country } // RunGeoipVerify is the child: args are the file, then the probe // addresses. It writes the report to w and returns the exit code (0 ok, 2 // refused). A panic or a hang here kills only this process. func RunGeoipVerify(args []string, w io.Writer) int { if len(args) < 1 { return 2 } // Measured: DB-IP City Lite (127 MB) verifies within 256 MiB of // private memory; 512 MiB leaves room for GeoLite2 and growth. if err := limitVerifierMemory(geoipVerifyDataLimit); err != nil { return 2 // without the bound this process must not parse anything } db, err := geo.OpenMMDBVerified(context.Background(), args[0]) if errors.Is(err, geo.ErrSearchTreeTooLarge) { return geoipVerifyExitTree } if err != nil { return 2 } defer db.Close() // Every record through the production schema, here where a pathological // one can only kill this process (code review R11-1). if err := db.CheckRecords(context.Background()); err != nil { return 2 } res := geoipVerifyResult{Source: db.Source(), BuildEpoch: db.BuildEpoch().Unix(), NodeCount: db.NodeCount()} for _, a := range args[1:] { if r, ok := db.Lookup(net.ParseIP(a)); ok && r.Country != "" { res.Countries++ } } if err := json.NewEncoder(w).Encode(res); err != nil { return 2 } return 0 } // geoipVerifyDataLimit is the child's private-memory cap (code review R10-1). const geoipVerifyDataLimit = 512 << 20 // geoipVerifyTimeout bounds the child; a var so a test need not wait. var geoipVerifyTimeout = 5 * time.Minute // geoipVerifyExec starts the child; tests point it at the test binary. var geoipVerifyExec = func(ctx context.Context, args ...string) *exec.Cmd { self, err := os.Executable() if err != nil { self = os.Args[0] } return exec.CommandContext(ctx, self, append([]string{GeoipVerifyVerb}, args...)...) } var ( errGeoipVerifyTimeout = errors.New("verification took too long") errGeoipVerifyTree = errors.New("malformed search tree") ) // geoipVerifyExitTree is the child's exit code for a search tree with far // more networks than nodes. const geoipVerifyExitTree = 3 // verifyInChild runs the child and returns its report. A crash, a refusal // or a malformed report is an invalid database; the deadline is its own // category; the caller's own cancellation is returned as it is. func verifyInChild(ctx context.Context, path string, probes []string) (geoipVerifyResult, error) { vctx, cancel := context.WithTimeout(ctx, geoipVerifyTimeout) defer cancel() cmd := geoipVerifyExec(vctx, append([]string{path}, probes...)...) var out bytes.Buffer cmd.Stdout = &cappedBuffer{buf: &out, max: 4 << 10} cmd.Stderr = io.Discard cmd.WaitDelay = 5 * time.Second err := cmd.Run() switch { case ctx.Err() != nil: return geoipVerifyResult{}, ctx.Err() case vctx.Err() != nil: return geoipVerifyResult{}, errGeoipVerifyTimeout case err != nil: var ee *exec.ExitError if errors.As(err, &ee) && ee.ExitCode() == geoipVerifyExitTree { return geoipVerifyResult{}, errGeoipVerifyTree } return geoipVerifyResult{}, fmt.Errorf("verifier: %w", err) } var res geoipVerifyResult if err := json.Unmarshal(out.Bytes(), &res); err != nil { return geoipVerifyResult{}, fmt.Errorf("verifier report: %w", err) } return res, nil } type cappedBuffer struct { buf *bytes.Buffer max int } func (c *cappedBuffer) Write(p []byte) (int, error) { if c.buf.Len()+len(p) > c.max { return 0, errors.New("verifier report too large") } return c.buf.Write(p) } // VerifyGeoIPFile checks a database in the verifier child; nil means it is // safe to open in this process with geo.OpenMMDBTrusted. cmd/console uses // it for the CONSOLE_GEOIP_DB override, so even an operator's broken file // cannot crash the console at startup. func VerifyGeoIPFile(ctx context.Context, path string) error { _, err := verifyInChild(ctx, path, nil) return err }