您當前的位置:首頁 > 舞蹈

Mysql讀寫分離+防止sql注入攻擊「GO原始碼剖析」

作者:由 周明 發表于 舞蹈時間:2020-03-08

一、讀寫分離和防止sql注入的必要性(foreword)

1、 讀寫分離:

一句話定義:讀寫分離,基本的原理是讓主資料庫處理事務性增、改、刪操作(INSERT、UPDATE、DELETE),而從資料庫處理SELECT查詢操作。資料庫複製被用來把事務性操作導致的變更同步到叢集中的從資料庫。

Mysql讀寫分離+防止sql注入攻擊「GO原始碼剖析」

圖1-1 主從讀寫分離示意圖

讀寫分離的好處:

(1) 增加冗餘

(2) 增加了機器的處理能力

(3) 對於讀操作為主的應用,使用讀寫分離是最好的場景,因為可以確保寫的伺服器壓力更小,而讀又可以接受點時間上的延遲。

2、 防止sql注入攻擊:

一句話定義:SQL注入攻擊(SQL Injection),簡稱注入攻擊,是Web開發中最常見的一種安全漏洞。可以用它來從資料庫獲取敏感資訊,或者利用資料庫的特性執行新增使用者,匯出檔案等一系列惡意操作,甚至有可能獲取資料庫乃至系統使用者最高許可權。

二、

最佳解決方案(what)

主庫用讀寫賬戶,從庫用只讀賬戶,建立mysql連結時使用引數interpolateParams=true

package

main

import

“database/sql”

“fmt”

_

“github。com/go-sql-driver/mysql”

func

main

()

{

//主庫用讀寫賬戶 建立mysql連結時使用引數interpolateParams=true

db_master

err

:=

sql

Open

“mysql”

“master:@tcp(127。0。0。1:3306)/db_test?charset=utf8&interpolateParams=true”

if

err

!=

nil

{

panic

err

Error

())

//只是舉例,真實使用中需要對錯誤進行處理和返回

}

defer

db_master

Close

()

//從庫用只讀賬戶 建立mysql連結時使用引數interpolateParams=true

db_slave

err

:=

sql

Open

“mysql”

“slave:@tcp(127。0。0。1:3306)/db_test?charset=utf8&interpolateParams=true”

if

err

!=

nil

{

panic

err

Error

())

}

defer

db_slave

Close

()

rows

err

:=

db_slave

Query

“SELECT * FROM tbl_user WHERE user_id = ?”

1

if

err

!=

nil

{

panic

err

Error

())

}

}

三、 幾種方案對比(why)

1、主從都使用

讀寫賬戶

,sql語句採用

字串拼接

方式

(廢棄)

既不能實現主從讀寫分離,也不能防止sql注入攻擊。

2、主從都使用

讀寫賬戶

,sql採用

prepare+execute

的方式

(存在風險)

可以防止sql注入攻擊,但不能實現主從讀寫分離,當併發量上來後主庫壓力會很大,存在風險。

3、主採用

讀寫賬戶

,從採用

只讀賬戶

,sql採用

prepare+execute

的方式

(會報錯)

(1)首先丟擲報錯:

Mysql讀寫分離+防止sql注入攻擊「GO原始碼剖析」

(2)報錯原因分析:

Mysql讀寫分離+防止sql注入攻擊「GO原始碼剖析」

圖1-2 報錯原因分析

錯誤原因:如果prepare的時候sql發給了從庫1,但是execute的時候因為從庫1延時較大,sql命令發給了從庫2,就會報上面這個錯誤(除此之外,預設proxy會隔斷時間切換備機,保證備機都能用到)。

4、主採用

讀寫賬戶

,從採用

只讀賬戶

,建立mysql連結時使用引數

interpolateParams=true

針對方案4,我們需要來看下底層原始碼:

這裡我們需要關注兩部分原始碼:

database/sql和go-sql-driver/mysql

,其中

database/sql

是golang針對資料庫抽象出來的一個標準庫,

go-sql-driver/mysql

是實現database/sql驅動介面的mysql驅動。

(1)我們一般寫查詢語句是這樣的

(舉個栗子)

rows, err := db_slave。Query(“SELECT * FROM tbl_user WHERE user_id = ?”, 1)

if err != nil {

panic(err。Error()) //只是舉例,真實使用中需要對錯誤進行處理和返回

}

我們知道prepared statement可以防止sql注入攻擊,上圖這樣的寫法看上去是使用的prepared statement方式,但到底是不是呢?我們繼續往底層走。

(2)database/sql中查詢介面是下面兩個方法(即

Query和QueryRow

// Query executes a query that returns rows, typically a SELECT。

// The args are for any placeholder parameters in the query。

func

db

*

DB

Query

query

string

args

。。。

interface

{})

*

Rows

error

{

return

db

QueryContext

context

Background

(),

query

args

。。。

}

注:Query執行查詢並返回多個數據行,這個查詢通常是一個select,方法中args引數用於填寫查詢語句中包含的佔位符的實際引數。

// QueryRow executes a query that is expected to return at most one row。

// QueryRow always returns a non-nil value。 Errors are deferred until

// Row‘s Scan method is called。

// If the query selects no rows, the *Row’s Scan will return ErrNoRows。

// Otherwise, the *Row‘s Scan scans the first selected row and discards

// the rest。

func

db

*

DB

QueryRow

query

string

args

。。。

interface

{})

*

Row

{

return

db

QueryRowContext

context

Background

(),

query

args

。。。

}

注:QueryRow與Query方法不同點是,執行一條查詢最多隻會返回一個數據行。

(3)發現寶藏:Query底層執行查詢的策略

queryDC

方法來看Query預設是不使用prepared statement方式的,只有在查詢時發生

driver.ErrSkip

錯誤才會啟用prepared statement繼續查詢。

注:從Query到queryDC方法經過的連接獲取和錯誤處理等邏輯不影響我們分析問題,可以忽略。

// queryDC executes a query on the given connection。

// The connection gets released by the releaseConn function。

// The ctx context is from a query method and the txctx context is from an

// optional transaction context。

func

db

*

DB

queryDC

ctx

txctx

context

Context

dc

*

driverConn

releaseConn

func

error

),

query

string

args

[]

interface

{})

*

Rows

error

{

queryerCtx

ok

:=

dc

ci

。(

driver

QueryerContext

var

queryer

driver

Queryer

if

ok

{

queryer

ok

=

dc

ci

。(

driver

Queryer

}

if

ok

{

var

nvdargs

[]

driver

NamedValue

var

rowsi

driver

Rows

var

err

error

withLock

dc

func

()

{

nvdargs

err

=

driverArgsConnLocked

dc

ci

nil

args

if

err

!=

nil

{

return

}

//核心查詢 不使用‘prepared Statement‘來執行

rowsi

err

=

ctxDriverQuery

ctx

queryerCtx

queryer

query

nvdargs

})

if

err

!=

driver

ErrSkip

{

//發生錯誤driver。ErrSkip才使用‘prepared Statement‘方式去查詢

if

err

!=

nil

{

//其他錯誤,關閉連結並報錯返回

releaseConn

err

return

nil

err

}

// Note: ownership of dc passes to the *Rows, to be freed

// with releaseConn。

rows

:=

&

Rows

{

dc

dc

releaseConn

releaseConn

rowsi

rowsi

}

rows

initContextClose

ctx

txctx

return

rows

nil

}

}

//發生錯誤driver。ErrSkip時,才會使用‘Prepared Statement‘方式再次執行查詢

var

si

driver

Stmt

var

err

error

withLock

dc

func

()

{

si

err

=

ctxDriverPrepare

ctx

dc

ci

query

//prepare

})

if

err

!=

nil

{

releaseConn

err

return

nil

err

}

ds

:=

&

driverStmt

{

Locker

dc

si

si

}

rowsi

err

:=

rowsiFromStatement

ctx

dc

ci

ds

args

。。。

//Statement

if

err

!=

nil

{

ds

Close

()

releaseConn

err

return

nil

err

}

// Note: ownership of ci passes to the *Rows, to be freed

// with releaseConn。

rows

:=

&

Rows

{

dc

dc

releaseConn

releaseConn

rowsi

rowsi

closeStmt

ds

}

rows

initContextClose

ctx

txctx

return

rows

nil

}

(4)什麼時候才會報driver。ErrSkip錯誤呢

答案就在go-sql-driver/mysql下的connection。go中真實Query方法中,請看大螢幕:

//mysql query底層實現

func

mc

*

mysqlConn

query

query

string

args

[]

driver

Value

*

textRows

error

{

if

mc

closed

IsSet

()

{

errLog

Print

ErrInvalidConn

return

nil

driver

ErrBadConn

}

if

len

args

!=

0

{

if

mc

cfg

InterpolateParams

{

//能不能不使用Prepared Statement,就看interpolateParams是不是true了

return

nil

driver

ErrSkip

}

// try client-side prepare to reduce roundtrip

prepared

err

:=

mc

interpolateParams

query

args

//在mysql客戶端側使用插值法實現防止sql注入攻擊

if

err

!=

nil

{

return

nil

err

}

query

=

prepared

}

// Send command

err

:=

mc

writeCommandPacketStr

comQuery

query

if

err

==

nil

{

// Read Result

var

resLen

int

resLen

err

=

mc

readResultSetHeaderPacket

()

if

err

==

nil

{

rows

:=

new

textRows

rows

mc

=

mc

if

resLen

==

0

{

rows

rs

done

=

true

switch

err

:=

rows

NextResultSet

();

err

{

case

nil

io

EOF

return

rows

nil

default

return

nil

err

}

}

// Columns

rows

rs

columns

err

=

mc

readColumns

resLen

return

rows

err

}

}

return

nil

mc

markBadConn

err

}

(5) 驅動程式是如何使用插值法防止sql注入攻擊的呢?

我們到interpolateParams方法中一探究竟

(我們只需要關注引數為string的情況)

func

mc

*

mysqlConn

interpolateParams

query

string

args

[]

driver

Value

string

error

{

// Number of ? should be same to len(args)

if

strings

Count

query

“?”

!=

len

args

{

return

“”

driver

ErrSkip

}

buf

err

:=

mc

buf

takeCompleteBuffer

()

if

err

!=

nil

{

// can not take the buffer。 Something must be wrong with the connection

errLog

Print

err

return

“”

ErrInvalidConn

}

buf

=

buf

[:

0

argPos

:=

0

for

i

:=

0

i

<

len

query

);

i

++

{

q

:=

strings

IndexByte

query

i

:],

’?‘

if

q

==

-

1

{

buf

=

append

buf

query

i

:]

。。。

break

}

buf

=

append

buf

query

i

i

+

q

。。。

i

+=

q

arg

:=

args

argPos

argPos

++

if

arg

==

nil

{

buf

=

append

buf

“NULL”

。。。

continue

}

switch

v

:=

arg

。(

type

{

case

int64

buf

=

strconv

AppendInt

buf

v

10

case

uint64

// Handle uint64 explicitly because our custom ConvertValue emits unsigned values

buf

=

strconv

AppendUint

buf

v

10

case

float64

buf

=

strconv

AppendFloat

buf

v

’g‘

-

1

64

case

bool

if

v

{

buf

=

append

buf

’1‘

}

else

{

buf

=

append

buf

’0‘

}

case

time

Time

if

v

IsZero

()

{

buf

=

append

buf

“’0000-00-00‘”

。。。

}

else

{

v

:=

v

In

mc

cfg

Loc

v

=

v

Add

time

Nanosecond

*

500

// To round under microsecond

year

:=

v

Year

()

year100

:=

year

/

100

year1

:=

year

%

100

month

:=

v

Month

()

day

:=

v

Day

()

hour

:=

v

Hour

()

minute

:=

v

Minute

()

second

:=

v

Second

()

micro

:=

v

Nanosecond

()

/

1000

buf

=

append

buf

[]

byte

{

’\‘’

digits10

year100

],

digits01

year100

],

digits10

year1

],

digits01

year1

],

‘-’

digits10

month

],

digits01

month

],

‘-’

digits10

day

],

digits01

day

],

‘ ’

digits10

hour

],

digits01

hour

],

‘:’

digits10

minute

],

digits01

minute

],

‘:’

digits10

second

],

digits01

second

],

}

。。。

if

micro

!=

0

{

micro10000

:=

micro

/

10000

micro100

:=

micro

/

100

%

100

micro1

:=

micro

%

100

buf

=

append

buf

[]

byte

{

‘。’

digits10

micro10000

],

digits01

micro10000

],

digits10

micro100

],

digits01

micro100

],

digits10

micro1

],

digits01

micro1

],

}

。。。

}

buf

=

append

buf

‘\’‘

}

case

json

RawMessage

buf

=

append

buf

’\‘’

if

mc

status

&

statusNoBackslashEscapes

==

0

{

buf

=

escapeBytesBackslash

buf

v

}

else

{

buf

=

escapeBytesQuotes

buf

v

}

buf

=

append

buf

‘\’‘

case

[]

byte

if

v

==

nil

{

buf

=

append

buf

“NULL”

。。。

}

else

{

buf

=

append

buf

“_binary’”

。。。

if

mc

status

&

statusNoBackslashEscapes

==

0

{

buf

=

escapeBytesBackslash

buf

v

}

else

{

buf

=

escapeBytesQuotes

buf

v

}

buf

=

append

buf

‘\’‘

}

case

string

//我們只需要關注引數為string的情況

buf

=

append

buf

’\‘’

if

mc

status

&

statusNoBackslashEscapes

==

0

{

buf

=

escapeStringBackslash

buf

v

}

else

{

buf

=

escapeStringQuotes

buf

v

}

buf

=

append

buf

‘\’‘

default

return

“”

driver

ErrSkip

}

if

len

buf

+

4

>

mc

maxAllowedPacket

{

return

“”

driver

ErrSkip

}

}

if

argPos

!=

len

args

{

return

“”

driver

ErrSkip

}

return

string

buf

),

nil

}

原始碼中escapeStringBackslash方法的字面意思是對字串引數轉義,

我們知道轉義特殊字元就是防止sql注入攻擊的有效方法之一

,本著刨根問底的科學探索精神讓我們再進到該方法中一探究竟~

//非常清晰,這裡就是在做字串中特殊字元的轉義編碼

// escapeStringBackslash is similar to escapeBytesBackslash but for string。

func

escapeStringBackslash

buf

[]

byte

v

string

[]

byte

{

pos

:=

len

buf

buf

=

reserveBuffer

buf

len

v

*

2

for

i

:=

0

i

<

len

v

);

i

++

{

c

:=

v

i

switch

c

{

case

’\x00‘

buf

pos

=

’\\‘

buf

pos

+

1

=

’0‘

pos

+=

2

case

’\n‘

buf

pos

=

’\\‘

buf

pos

+

1

=

’n‘

pos

+=

2

case

’\r‘

buf

pos

=

’\\‘

buf

pos

+

1

=

’r‘

pos

+=

2

case

’\x1a‘

buf

pos

=

’\\‘

buf

pos

+

1

=

’Z‘

pos

+=

2

case

’\‘’

buf

pos

=

‘\\’

buf

pos

+

1

=

‘\’‘

pos

+=

2

case

’“‘

buf

pos

=

’\\‘

buf

pos

+

1

=

’”‘

pos

+=

2

case

’\\‘

buf

pos

=

’\\‘

buf

pos

+

1

=

’\\‘

pos

+=

2

default

buf

pos

=

c

pos

++

}

}

return

buf

[:

pos

}

這裡就是在做字串中特殊字元的轉義編碼,因此使用引數

interpolateParams=true

可以防止sql注入攻擊,並且因為沒有使用prepared statement方式,當採用主從讀寫分離方式時,也不會再報出方案3中的錯誤,既實現了主從讀寫分離也可以防止sql注入攻擊,是最佳解決方案。

(6)

interpolateParams=true

使用注意事項

Mysql讀寫分離+防止sql注入攻擊「GO原始碼剖析」

可以看到官方文件最後給出一個補充說明,

interpolateParams=true不可以與以下多位元組編碼共同使用(multibyte encodings BIG5, CP932, GB2312, GBK or SJIS),因為他們會引起sql注入脆弱性。

感謝閱讀,如果你感覺本文能幫到你,歡迎點贊、評論和收藏本文,鄙人會再接再厲,為您帶來更多系統架構和機器學習相關的知識分享~

標簽: buf  err  SQL  POS  driver