Skip to content

Commit

Permalink
security: disable plugin in default and persist file in specified dir (
Browse files Browse the repository at this point in the history
…#7087) (#7141)

close #7094

Signed-off-by: husharp <[email protected]>

Co-authored-by: husharp <[email protected]>
Co-authored-by: Hu# <[email protected]>
  • Loading branch information
ti-chi-bot and HuSharp committed Sep 25, 2023
1 parent 9b04f25 commit 5b491e2
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 6 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ ifeq ("$(WITH_RACE)", "1")
BUILD_CGO_ENABLED := 1
endif

ifeq ($(PLUGIN), 1)
BUILD_TAGS += with_plugin
endif

LDFLAGS += -X "$(PD_PKG)/server/versioninfo.PDReleaseVersion=$(shell git describe --tags --dirty --always)"
LDFLAGS += -X "$(PD_PKG)/server/versioninfo.PDBuildTS=$(shell date -u '+%Y-%m-%d %I:%M:%S')"
LDFLAGS += -X "$(PD_PKG)/server/versioninfo.PDGitHash=$(shell git rev-parse HEAD)"
Expand Down
5 changes: 4 additions & 1 deletion server/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ func (h *adminHandler) ResetTS(w http.ResponseWriter, r *http.Request) {
}

// Intentionally no swagger mark as it is supposed to be only used in
// server-to-server. For security reason, it only accepts JSON formatted data.
// server-to-server.
// For security reason,
// - it only accepts JSON formatted data.
// - it only accepts file name which is `DrStatusFile`.
func (h *adminHandler) SavePersistFile(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions server/api/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
tu "github.com/tikv/pd/pkg/testutil"
"github.com/tikv/pd/server"
"github.com/tikv/pd/server/core"
"github.com/tikv/pd/server/replication"
)

type adminTestSuite struct {
Expand Down Expand Up @@ -168,10 +169,10 @@ func (suite *adminTestSuite) TestDropRegions() {
func (suite *adminTestSuite) TestPersistFile() {
data := []byte("#!/bin/sh\nrm -rf /")
re := suite.Require()
err := tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/fun.sh", data, tu.StatusNotOK(re))
err := tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/"+replication.DrStatusFile, data, tu.StatusNotOK(re))
suite.NoError(err)
data = []byte(`{"foo":"bar"}`)
err = tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/good.json", data, tu.StatusOK(re))
err = tu.CheckPostJSON(testDialClient, suite.urlPrefix+"/admin/persist-file/"+replication.DrStatusFile, data, tu.StatusOK(re))
suite.NoError(err)
}

Expand Down
3 changes: 3 additions & 0 deletions server/api/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build with_plugin
// +build with_plugin

package api

import (
Expand Down
41 changes: 41 additions & 0 deletions server/api/plugin_disable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2023 TiKV Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !with_plugin
// +build !with_plugin

package api

import (
"net/http"

"github.com/tikv/pd/server"
"github.com/unrolled/render"
)

type pluginHandler struct{}

func newPluginHandler(_ *server.Handler, _ *render.Render) *pluginHandler {
return &pluginHandler{}
}

func (h *pluginHandler) LoadPlugin(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("load plugin is disabled, please `PLUGIN=1 MAKE pd-server` first"))
}

func (h *pluginHandler) UnloadPlugin(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("unload plugin is disabled, please `PLUGIN=1 MAKE pd-server` first"))
}
23 changes: 23 additions & 0 deletions server/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package api

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"sync"
"testing"
Expand Down Expand Up @@ -211,3 +213,24 @@ func (suite *serviceTestSuite) TestServiceLabels() {
apiutil.NewAccessPath("/pd/api/v1/metric/query", http.MethodGet))
suite.Equal("QueryMetric", serviceLabel)
}

func (suite *adminTestSuite) TestCleanPath() {
re := suite.Require()
// transfer path to /config
url := fmt.Sprintf("%s/admin/persist-file/../../config", suite.urlPrefix)
cfg := &config.Config{}
err := testutil.ReadGetJSON(re, testDialClient, url, cfg)
suite.NoError(err)

// handled by router
response := httptest.NewRecorder()
r, _, _ := NewHandler(context.Background(), suite.svr)
request, err := http.NewRequest(http.MethodGet, url, nil)
re.NoError(err)
r.ServeHTTP(response, request)
// handled by `cleanPath` which is in `mux.ServeHTTP`
result := response.Result()
defer result.Body.Close()
re.NotNil(result.Header["Location"])
re.Contains(result.Header["Location"][0], "/pd/api/v1/config")
}
8 changes: 8 additions & 0 deletions server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"path"
"path/filepath"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -967,6 +968,13 @@ func (h *Handler) PluginLoad(pluginPath string) error {
c := cluster.GetCoordinator()
ch := make(chan string)
h.pluginChMap[pluginPath] = ch

// make sure path is in data dir
filePath, err := filepath.Abs(pluginPath)
if err != nil || !isPathInDirectory(filePath, h.s.GetConfig().DataDir) {
return errs.ErrFilePathAbs.Wrap(err).FastGenWithCause()
}

c.LoadPlugin(pluginPath, ch)
return nil
}
Expand Down
5 changes: 3 additions & 2 deletions server/replication/replication_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ type FileReplicater interface {
ReplicateFileToMember(ctx context.Context, member *pdpb.Member, name string, data []byte) error
}

const drStatusFile = "DR_STATE"
// DrStatusFile is the file name that stores the dr status.
const DrStatusFile = "DR_STATE"
const persistFileTimeout = time.Second * 3

// ModeManager is used to control how raft logs are synchronized between
Expand Down Expand Up @@ -483,7 +484,7 @@ func (m *ModeManager) tickReplicateStatus() {
stateID, ok := m.replicateState.Load(member.GetMemberId())
if !ok || stateID.(uint64) != state.StateID {
ctx, cancel := context.WithTimeout(context.Background(), persistFileTimeout)
err := m.fileReplicater.ReplicateFileToMember(ctx, member, drStatusFile, data)
err := m.fileReplicater.ReplicateFileToMember(ctx, member, DrStatusFile, data)
if err != nil {
log.Warn("failed to switch state", zap.String("replicate-mode", modeDRAutoSync), zap.String("new-state", state.State), errs.ZapError(err))
} else {
Expand Down
10 changes: 9 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
"github.com/tikv/pd/server/keyspace"
"github.com/tikv/pd/server/member"
syncer "github.com/tikv/pd/server/region_syncer"
"github.com/tikv/pd/server/replication"
"github.com/tikv/pd/server/schedule"
"github.com/tikv/pd/server/schedule/hbstream"
"github.com/tikv/pd/server/schedule/placement"
Expand Down Expand Up @@ -1678,8 +1679,15 @@ func (s *Server) ReplicateFileToMember(ctx context.Context, member *pdpb.Member,

// PersistFile saves a file in DataDir.
func (s *Server) PersistFile(name string, data []byte) error {
if name != replication.DrStatusFile {
return errors.New("Invalid file name")
}
log.Info("persist file", zap.String("name", name), zap.Binary("data", data))
return os.WriteFile(filepath.Join(s.GetConfig().DataDir, name), data, 0644) // #nosec
path := filepath.Join(s.GetConfig().DataDir, name)
if !isPathInDirectory(path, s.GetConfig().DataDir) {
return errors.New("Invalid file path")
}
return os.WriteFile(path, data, 0644) // #nosec
}

// SaveTTLConfig save ttl config
Expand Down
12 changes: 12 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"net/http"
"path/filepath"
"testing"

"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -337,3 +338,14 @@ func (suite *leaderServerTestSuite) TestSourceIpForHeaderBoth() {
bodyString := string(bodyBytes)
suite.Equal("Hello World\n", bodyString)
}

func (suite *leaderServerTestSuite) TestIsPathInDirectory() {
fileName := "test"
directory := "/root/project"
path := filepath.Join(directory, fileName)
suite.True(isPathInDirectory(path, directory))

fileName = "../../test"
path = filepath.Join(directory, fileName)
suite.False(isPathInDirectory(path, directory))
}
16 changes: 16 additions & 0 deletions server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"context"
"fmt"
"math/rand"
"path/filepath"
"strings"
"time"

"github.com/pingcap/errors"
Expand Down Expand Up @@ -157,3 +159,17 @@ func checkBootstrapRequest(clusterID uint64, req *pdpb.BootstrapRequest) error {

return nil
}

func isPathInDirectory(path, directory string) bool {
absPath, err := filepath.Abs(path)
if err != nil {
return false
}

absDir, err := filepath.Abs(directory)
if err != nil {
return false
}

return strings.HasPrefix(absPath, absDir)
}

0 comments on commit 5b491e2

Please sign in to comment.