溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何進(jìn)行GraphQL的分析

發(fā)布時(shí)間:2021-12-20 11:48:16 來(lái)源:億速云 閱讀:160 作者:柒染 欄目:網(wǎng)絡(luò)管理

這篇文章給大家介紹如何進(jìn)行GraphQL的分析,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

說(shuō)在前面的話

下面以GraphQL中一些容易讓初學(xué)者與典型Web API(為了便于理解,下文以目前流行的RESTful API為例代指)混淆或錯(cuò)誤理解的概念特性進(jìn)行內(nèi)容劃分,由我從安全的角度拋出GraphQL應(yīng)該注意的幾點(diǎn)安全問(wèn)題,而@圖南則會(huì)更多的從開(kāi)發(fā)的角度給出他在實(shí)際使用過(guò)程中總結(jié)的最佳實(shí)踐。

另外,需要提前聲明的是,文中我使用的后端開(kāi)發(fā)語(yǔ)言是Go,@圖南使用的是Node.js,前端統(tǒng)一為React(GraphQL客戶端為Apollo),請(qǐng)大家自行消化。

Let’s Go!

GraphQL簡(jiǎn)介

有些同學(xué)是不是根本沒(méi)聽(tīng)過(guò)這個(gè)玩意?我們先來(lái)看看正在使用它的大客戶們:

如何進(jìn)行GraphQL的分析是不是值得我們花幾分鐘對(duì)它做個(gè)簡(jiǎn)單的了解了?XD 

什么是GraphQL

簡(jiǎn)單的說(shuō),GraphQL是由Facebook創(chuàng)造并開(kāi)源的一種用于API的查詢語(yǔ)言。

如何進(jìn)行GraphQL的分析再引用官方文案來(lái)幫助大家理解一下GraphQL的特點(diǎn):    

1.請(qǐng)求你所要的數(shù)據(jù),不多不少

向你的API發(fā)出一個(gè)GraphQL請(qǐng)求就能準(zhǔn)確獲得你想要的數(shù)據(jù),不多不少。GraphQL查詢總是返回可預(yù)測(cè)的結(jié)果。使用GraphQL的應(yīng)用可以工作得又快又穩(wěn),因?yàn)榭刂茢?shù)據(jù)的是應(yīng)用,而不是服務(wù)器。

2.獲取多個(gè)資源,只用一個(gè)請(qǐng)求

GraphQL查詢不僅能夠獲得資源的屬性,還能沿著資源間引用進(jìn)一步查詢。典型的RESTful API請(qǐng)求多個(gè)資源時(shí)得載入多個(gè)URL,而GraphQL可以通過(guò)一次請(qǐng)求就獲取你應(yīng)用所需的所有數(shù)據(jù)。

3.描述所有的可能,類(lèi)型系統(tǒng)

GraphQL基于類(lèi)型和字段的方式進(jìn)行組織,而非入口端點(diǎn)。你可以通過(guò)一個(gè)單一入口端點(diǎn)得到你所有的數(shù)據(jù)能力。GraphQL使用類(lèi)型來(lái)保證應(yīng)用只請(qǐng)求可能的數(shù)據(jù),還提供了清晰的輔助性錯(cuò)誤信息。

GraphQL核心組成部分   

1.Type

用于描述接口的抽象數(shù)據(jù)模型,有Scalar(標(biāo)量)和Object(對(duì)象)兩種,Object由Field組成,同時(shí)Field也有自己的Type。

2.Schema

用于描述接口獲取數(shù)據(jù)的邏輯,類(lèi)比RESTful中的每個(gè)獨(dú)立資源URI。

3.Query

用于描述接口的查詢類(lèi)型,有Query(查詢)、Mutation(更改)和Subscription(訂閱)三種。

4.Resolver

用于描述接口中每個(gè)Query的解析邏輯,部分GraphQL引擎還提供Field細(xì)粒度的Resolver(想要詳細(xì)了解的同學(xué)請(qǐng)閱讀GraphQL官方文檔)。

GraphQL VS. RESTful

GraphQL沒(méi)有過(guò)多依賴(lài)HTTP協(xié)議,它有一套自己的解析引擎來(lái)幫助前后端使用GraphQL查詢語(yǔ)法。同時(shí)它是單路由形態(tài),查詢內(nèi)容完全根據(jù)前端請(qǐng)求對(duì)象和字段而定,前后端分離較明顯。

用一張圖來(lái)對(duì)比一下:

如何進(jìn)行GraphQL的分析

身份認(rèn)證與權(quán)限控制不當(dāng)

@gyyyy:

前面說(shuō)到,GraphQL多了一個(gè)中間層對(duì)它定義的查詢語(yǔ)言進(jìn)行語(yǔ)法解析執(zhí)行等操作,與RESTful這種充分利用HTTP協(xié)議本身特性完成聲明使用的API設(shè)計(jì)不同,Schema、Resolver等種種定義會(huì)讓開(kāi)發(fā)者對(duì)它的存在感知較大,間接的增加了對(duì)它理解的復(fù)雜度,加上它本身的單路由形態(tài),很容易導(dǎo)致開(kāi)發(fā)者在不完全了解其特性和內(nèi)部運(yùn)行機(jī)制的情況下,錯(cuò)誤實(shí)現(xiàn)甚至忽略API調(diào)用時(shí)的授權(quán)鑒權(quán)行為。

在官方的描述中,GraphQL和RESTful API一樣,建議開(kāi)發(fā)者將授權(quán)邏輯委托給業(yè)務(wù)邏輯層:

如何進(jìn)行GraphQL的分析在沒(méi)有對(duì)GraphQL中各個(gè)Query和Mutation做好授權(quán)鑒權(quán)時(shí),同樣可能會(huì)被攻擊者非法請(qǐng)求到一些非預(yù)期接口,執(zhí)行高危操作,如查詢所有用戶的詳細(xì)信息:    

    query GetAllUsers {
        users {
            _id
            username
            password
            idCard
            mobilePhone
            email
        }
    }

這幾乎是使用任何API技術(shù)都無(wú)法避免的一個(gè)安全問(wèn)題,因?yàn)樗cAPI本身的職能并沒(méi)有太大的關(guān)系,API不需要背這個(gè)鍋,但由此問(wèn)題帶來(lái)的并發(fā)癥卻不容小覷。

信息泄露

對(duì)于這種未授權(quán)或越權(quán)訪問(wèn)漏洞的挖掘利用方式,大家一定都很清楚了,一般情況下我們都會(huì)期望盡可能獲取到比較全量的API來(lái)進(jìn)行進(jìn)一步的分析。在RESTful API中,我們可能需要通過(guò)代理、爬蟲(chóng)等技術(shù)來(lái)抓取API。而隨著Web 2.0時(shí)代的到來(lái),各種強(qiáng)大的前端框架、運(yùn)行時(shí)DOM事件更新等技術(shù)使用頻率的增加,更使得我們不得不動(dòng)用到如Headless等技術(shù)來(lái)提高對(duì)API的獲取覆蓋率。

但與RESTful API不同的是,GraphQL自帶強(qiáng)大的內(nèi)省自檢機(jī)制,可以直接獲取后端定義的所有接口信息。比如通過(guò)__schema查詢所有可用對(duì)象:

    {        __schema {            types {                name            }        }    }

通過(guò)__type查詢指定對(duì)象的所有字段:

    {        __type(name: "User") {            name            fields {                name                type {                    name                }            }        }    }

這里我通過(guò)graphql-go/graphql的源碼簡(jiǎn)單分析一下GraphQL的解析執(zhí)行流程和內(nèi)省機(jī)制,幫助大家加深理解:

1.GraphQL路由節(jié)點(diǎn)在拿到HTTP的請(qǐng)求參數(shù)后,創(chuàng)建Params對(duì)象,并調(diào)用Do()完成解析執(zhí)行操作返回結(jié)果:

    params := graphql.Params{        Schema:         *h.Schema,        RequestString:  opts.Query,        VariableValues: opts.Variables,        OperationName:  opts.OperationName,        Context:        ctx,    }    result := graphql.Do(params)

2.調(diào)用Parser()把params.RequestString轉(zhuǎn)換為GraphQL的AST文檔后,將AST和Schema一起交給ValidateDocument()進(jìn)行校驗(yàn)(主要校驗(yàn)是否符合Schema定義的參數(shù)、字段、類(lèi)型等)。

3.代入AST重新封裝ExecuteParams對(duì)象,傳入Execute()中開(kāi)始執(zhí)行當(dāng)前GraphQL語(yǔ)句。

具體的執(zhí)行細(xì)節(jié)就不展開(kāi)了,但是我們關(guān)心的內(nèi)省去哪了?原來(lái)在GraphQL引擎初始化時(shí),會(huì)定義三個(gè)帶缺省Resolver的元字段:

    SchemaMetaFieldDef = &FieldDefinition{ // __schema:查詢當(dāng)前類(lèi)型定義的模式,無(wú)參數(shù)        Name:        "__schema",        Type:        NewNonNull(SchemaType),        Description: "Access the current type schema of this server.",        Args:        []*Argument{},        Resolve: func(p ResolveParams) (interface{}, error) {            return p.Info.Schema, nil        },    }    TypeMetaFieldDef = &FieldDefinition{ // __type:查詢指定類(lèi)型的詳細(xì)信息,字符串類(lèi)型參數(shù)name        Name:        "__type",        Type:        TypeType,        Description: "Request the type information of a single type.",        Args: []*Argument{            {                PrivateName: "name",                Type:        NewNonNull(String),            },        },        Resolve: func(p ResolveParams) (interface{}, error) {            name, ok := p.Args["name"].(string)            if !ok {                return nil, nil            }            return p.Info.Schema.Type(name), nil        },    }    TypeNameMetaFieldDef = &FieldDefinition{ // __typename:查詢當(dāng)前對(duì)象類(lèi)型名稱(chēng),無(wú)參數(shù)        Name:        "__typename",        Type:        NewNonNull(String),        Description: "The name of the current Object type at runtime.",        Args:        []*Argument{},        Resolve: func(p ResolveParams) (interface{}, error) {            return p.Info.ParentType.Name(), nil        },    }

當(dāng)resolveField()解析到元字段時(shí),會(huì)調(diào)用其缺省Resolver,觸發(fā)GraphQL的內(nèi)省邏輯。

自動(dòng)綁定(非預(yù)期和廢棄字段)   

GraphQL為了考慮接口在版本演進(jìn)時(shí)能夠向下兼容,還有一個(gè)對(duì)于應(yīng)用開(kāi)發(fā)而言比較友善的特性:『API演進(jìn)無(wú)需劃分版本』。

由于GraphQL是根據(jù)前端請(qǐng)求的字段進(jìn)行數(shù)據(jù)回傳,后端Resolver的響應(yīng)包含對(duì)應(yīng)字段即可,因此后端字段擴(kuò)展對(duì)前端無(wú)感知無(wú)影響,前端增加查詢字段也只要在后端定義的字段范圍內(nèi)即可。同時(shí)GraphQL也為字段刪除提供了『廢棄』方案,如Go的graphql包在字段中增加DeprecationReason屬性,Apollo的@deprecated標(biāo)識(shí)等。

這種特性非常方便的將前后端進(jìn)行了分離,但如果開(kāi)發(fā)者本身安全意識(shí)不夠強(qiáng),設(shè)計(jì)的API不夠合理,就會(huì)埋下了很多安全隱患。我們用開(kāi)發(fā)項(xiàng)目中可能會(huì)經(jīng)常遇到的需求場(chǎng)景來(lái)重現(xiàn)一下。

假設(shè)小明在應(yīng)用中已經(jīng)定義好了查詢用戶基本信息的API:

  graphql.Field{        Type: graphql.NewObject(graphql.ObjectConfig{            Name:        "User",            Description: "用戶信息",            Fields: graphql.Fields{                "_id": &graphql.Field{Type: graphql.Int},                "username": &graphql.Field{Type: graphql.String},                "email": &graphql.Field{Type: graphql.String},            },        }),        Args: graphql.FieldConfigArgument{            "username": &graphql.ArgumentConfig{Type: graphql.String},        },        Resolve: func(params graphql.ResolveParams) (result interface{}, err error) {            // ...        },    }

小明獲得新的需求描述,『管理員可以查詢指定用戶的詳細(xì)信息』,為了方便(也經(jīng)常會(huì)為了方便),于是在原有接口上新增了幾個(gè)字段:

    graphql.Field{        Type: graphql.NewObject(graphql.ObjectConfig{            Name:        "User",            Description: "用戶信息",            Fields: graphql.Fields{                "_id": &graphql.Field{Type: graphql.Int},                "username": &graphql.Field{Type: graphql.String},                "password": &graphql.Field{Type: graphql.String}, // 新增 用戶密碼 字段                "idCard": &graphql.Field{Type: graphql.String}, // 新增 用戶身份證號(hào) 字段                "mobilePhone": &graphql.Field{Type: graphql.String}, // 新增 用戶手機(jī)號(hào) 字段                "email": &graphql.Field{Type: graphql.String},            },        }),        Args: graphql.FieldConfigArgument{            "username": &graphql.ArgumentConfig{Type: graphql.String},        },        Resolve: func(params graphql.ResolveParams) (result interface{}, err error) {            // ...        },    }

如果此時(shí)小明沒(méi)有在字段細(xì)粒度上進(jìn)行權(quán)限控制(也暫時(shí)忽略其他權(quán)限問(wèn)題),攻擊者可以輕易的通過(guò)內(nèi)省發(fā)現(xiàn)這幾個(gè)本不該被普通用戶查看到的字段,并構(gòu)造請(qǐng)求進(jìn)行查詢(實(shí)際開(kāi)發(fā)中也經(jīng)常容易遺留一些測(cè)試字段,在GraphQL強(qiáng)大的內(nèi)省機(jī)制面前這無(wú)疑是非常危險(xiǎn)的。如果熟悉Spring自動(dòng)綁定漏洞的同學(xué),也會(huì)發(fā)現(xiàn)它們之間有一部分相似的地方)。

故事繼續(xù),當(dāng)小明發(fā)現(xiàn)這種做法欠妥時(shí),他決定廢棄這幾個(gè)字段:

    // ...    "password": &graphql.Field{Type: graphql.String, DeprecationReason: "安全性問(wèn)題"},    "idCard": &graphql.Field{Type: graphql.String, DeprecationReason: "安全性問(wèn)題"},    "mobilePhone": &graphql.Field{Type: graphql.String, DeprecationReason: "安全性問(wèn)題"},    // ...

接著,他又用上面的__type做了一次內(nèi)省,很好,廢棄字段查不到了,通知前端回滾查詢語(yǔ)句,問(wèn)題解決,下班回家(GraphQL的優(yōu)勢(shì)立刻凸顯出來(lái))。

熟悉安全攻防套路的同學(xué)都知道,很多的攻擊方式(尤其在Web安全中)都是利用了開(kāi)發(fā)、測(cè)試、運(yùn)維的知識(shí)盲點(diǎn)(如果你想問(wèn)這些盲點(diǎn)的產(chǎn)生原因,我只能說(shuō)是因?yàn)檎G闆r下根本用不到,所以不深入研究基本不會(huì)去刻意關(guān)注)。如果開(kāi)發(fā)者沒(méi)有很仔細(xì)的閱讀GraphQL官方文檔,特別是內(nèi)省這一章節(jié)的內(nèi)容,就可能不知道,通過(guò)指定includeDeprecated參數(shù)為true,__type仍然可以將廢棄字段暴露出來(lái):

    {        __type(name: "User") {            name            fields(includeDeprecated: true) {                name                isDeprecated                type {                    name                }            }        }    }

而且由于小明沒(méi)有對(duì)Resolver做修改,廢棄字段仍然可以正常參與查詢(兼容性惹的禍),故事結(jié)束。

正如p牛所言,『GraphQL是一門(mén)自帶文檔的技術(shù)』。可這也使得授權(quán)鑒權(quán)環(huán)節(jié)一旦出現(xiàn)紕漏,GraphQL背后的應(yīng)用所面臨的安全風(fēng)險(xiǎn)會(huì)比典型Web API大得多。

@圖南:

GraphQL并沒(méi)有規(guī)定任何身份認(rèn)證和權(quán)限控制的相關(guān)內(nèi)容,這是個(gè)好事情,因?yàn)槲覀兛梢愿`活的在應(yīng)用中實(shí)現(xiàn)各種粒度的認(rèn)證和權(quán)限。但是,在我的開(kāi)發(fā)過(guò)程中發(fā)現(xiàn),初學(xué)者經(jīng)常會(huì)忽略GraphQL的認(rèn)證,會(huì)寫(xiě)出一些裸奔的接口或者無(wú)效認(rèn)證的接口。那么我就在這里詳細(xì)說(shuō)一下GraphQL的認(rèn)證方式。

獨(dú)立認(rèn)證終端(RESTful)

如果后端本身支持RESTful或者有專(zhuān)門(mén)的認(rèn)證服務(wù)器,可以修改少量代碼就能實(shí)現(xiàn)GraphQL接口的認(rèn)證。這種認(rèn)證方式是最通用同時(shí)也是官方比較推薦的。

以JWT認(rèn)證為例,將整個(gè)GraphQL路由加入JWT認(rèn)證,開(kāi)放兩個(gè)RESTful接口做登錄和注冊(cè)用,登錄和注冊(cè)的具體邏輯不再贅述,登錄后返回JWT Token:

如何進(jìn)行GraphQL的分析

如何進(jìn)行GraphQL的分析

設(shè)置完成后,請(qǐng)求GraphQL接口需要先進(jìn)行登錄操作,然后在前端配置好認(rèn)證請(qǐng)求頭來(lái)訪問(wèn)GraphQL接口,以curl代替前端請(qǐng)求登錄RESTful接口:

    curl -X POST http://localhost:4000/login -H 'cache-control: no-cache' -H 'content-type: application/x-www-form-urlencoded' -d 'username=user1&password=123456'    {"message":"登錄成功","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7Il9pZCI6IjViNWU1NDcwN2YyZGIzMDI0YWJmOTY1NiIsInVzZXJuYW1lIjoidXNlcjEiLCJwYXNzd29yZCI6IiQyYSQwNSRqekROOGFQbEloRzJlT1A1ZW9JcVFPRzg1MWdBbWY0NG5iaXJaM0Y4NUdLZ3pVL3lVNmNFYSJ9LCJleHAiOjE1MzI5MTIyOTEsImlhdCI6MTUzMjkwODY5MX0.Uhd_EkKUEDkI9cdnYlOC7wSYZdYLQLFCb01WhSBeTpY"}

以GraphiQL(GraphQL開(kāi)發(fā)者調(diào)試工具,大部分GraphQL引擎自帶,默認(rèn)開(kāi)啟)代替前端請(qǐng)求GraphQL接口,要先設(shè)置認(rèn)證請(qǐng)求頭:

如何進(jìn)行GraphQL的分析如何進(jìn)行GraphQL的分析

在GraphQL內(nèi)認(rèn)證

如果GraphQL后端只能支持GraphQL不能支持RESTful,或者全部請(qǐng)求都需要使用GraphQL,也可以用GraphQL構(gòu)造login接口提供Token。

如下面例子,構(gòu)造login的Query Schema, 由返回值中攜帶Token:

    type Query {        login(            username: String!            password: String!        ): LoginMsg    }    type LoginMsg {        message: String        token: String    }

在Resolver中提供登錄邏輯:

    import bcrypt from 'bcryptjs';    import jsonwebtoken from 'jsonwebtoken';    export const login = async (_, args, context) => {        const db = await context.getDb();        const { username, password } = args;        const user = await db.collection('User').findOne({ username: username });        if (await bcrypt.compare(password, user.password)) {            return {                message: 'Login success',                token: jsonwebtoken.sign({                    user: user,                    exp: Math.floor(Date.now() / 1000) + (60 * 60), // 60 seconds * 60 minutes = 1 hour                }, 'your secret'),            };        }    }

登錄成功后,我們繼續(xù)把Token設(shè)置在請(qǐng)求頭中,請(qǐng)求GraphQL的其他接口。這時(shí)我們要對(duì)ApolloServer進(jìn)行如下配置:

    const server = new ApolloServer({        typeDefs: schemaText,        resolvers: resolverMap,        context: ({ ctx }) => {            const token = ctx.req.headers.authorization || '';            const user = getUser(token);            return {                ...user,                ...ctx,                ...app.context            };        },    });

實(shí)現(xiàn)getUser函數(shù):

    const getUser = (token) => {        let user = null;        const parts = token.split(' ');        if (parts.length === 2) {            const scheme = parts[0];            const credentials = parts[1];            if (/^Bearer$/i.test(scheme)) {                token = credentials;                try {                    user = jwt.verify(token, JWT_SECRET);                    console.log(user);                } catch (e) {                    console.log(e);                }            }        }        return user    }

配置好ApolloServer后,在Resolver中校驗(yàn)user:

    import { ApolloError, ForbiddenError, AuthenticationError } from 'apollo-server';    export const blogs = async (_, args, context) => {        const db = await context.getDb();        const user = context.user;        if(!user) {            throw new AuthenticationError("You must be logged in to see blogs");        }        const { blogId } = args;        const cursor = {};        if (blogId) {            cursor['_id'] = blogId;        }        const blogs = await db            .collection('blogs')            .find(cursor)            .sort({ publishedAt: -1 })            .toArray();        return blogs;    }

這樣我們即完成了通過(guò)GraphQL認(rèn)證的主要代碼。繼續(xù)使用GraphiQL代替前端請(qǐng)求GraphQL登錄接口:

如何進(jìn)行GraphQL的分析得到Token后,設(shè)置Token到請(qǐng)求頭 完成后續(xù)操作。如果請(qǐng)求頭失效,則得不到數(shù)據(jù):    

如何進(jìn)行GraphQL的分析

權(quán)限控制

在認(rèn)證過(guò)程中,我們只是識(shí)別請(qǐng)求是不是由合法用戶發(fā)起。權(quán)限控制可以讓我們?yōu)橛脩舴峙洳煌牟榭礄?quán)限和操作權(quán)限。如上,我們已經(jīng)將user放入GraphQL Sever的context中。而context的內(nèi)容又是我們可控的,因此context中的user既可以是{ loggedIn: true },又可以是{ user: { _id: 12345, roles: ['user', 'admin'] } }。大家應(yīng)該知道如何在Resolver中實(shí)現(xiàn)權(quán)限控制了吧,簡(jiǎn)單的舉個(gè)例子:

  users: (root, args, context) => {        if (!context.user || !context.user.roles.includes('admin'))            throw ForbiddenError("You must be an administrator to see all Users");        return User.getAll();    }

GraphQL注入

@gyyyy:

有語(yǔ)法就會(huì)有解析,有解析就會(huì)有結(jié)構(gòu)和順序,有結(jié)構(gòu)和順序就會(huì)有注入。

前端使用變量構(gòu)建帶參查詢語(yǔ)句:

    const id = props.match.params.id;    const queryUser = gql`{        user(_id: ${id}) {            _id            username            email        }    }`

name的值會(huì)在發(fā)出GraphQL查詢請(qǐng)求前就被拼接進(jìn)完整的GraphQL語(yǔ)句中。攻擊者對(duì)name注入惡意語(yǔ)句:

    -1)%7B_id%7Dhack%3Auser(username%3A"admin")%7Bpassword%23

可能GraphQL語(yǔ)句的結(jié)構(gòu)就被改變了:

    {        user(_id: -1) {            _id        }        hack: user(username: "admin") {            password #) {            _id            username            email        }    }

因此,帶參查詢一定要保證在后端GraphQL引擎解析時(shí),原語(yǔ)句結(jié)構(gòu)不變,參數(shù)值以變量的形式被傳入,由解析器實(shí)時(shí)賦值解析。

@圖南:

幸運(yùn)的是,GraphQL同時(shí)提供了『參數(shù)』和『變量』給我們使用。我們可以將參數(shù)值的拼接過(guò)程轉(zhuǎn)交給后端GraphQL引擎,前端就像進(jìn)行參數(shù)化查詢一樣。

例如,我們定義一個(gè)帶變量的Query:

    type Query {        user(            username: String!        ): User    }

請(qǐng)求時(shí)傳入變量:

    query GetUser($name: String!) {        user(username: $name) {            _id            username            email        }    }    // 變量    {"name": "some username"}

拒絕服務(wù)

@gyyyy:

做過(guò)代碼調(diào)試的同學(xué)可能會(huì)注意過(guò),在觀察的變量中存在相互關(guān)聯(lián)的對(duì)象時(shí),可以對(duì)它們進(jìn)行無(wú)限展開(kāi)(比如一些Web框架的Request-Response對(duì))。如果這個(gè)關(guān)聯(lián)關(guān)系不是引用而是值,就有可能出現(xiàn)OOM等問(wèn)題導(dǎo)致運(yùn)算性能下降甚至應(yīng)用運(yùn)行中斷。同理,在一些動(dòng)態(tài)求值的邏輯中也會(huì)存在這類(lèi)問(wèn)題,比如XXE的拒絕服務(wù)。

GraphQL中也允許對(duì)象間包含組合的嵌套關(guān)系存在,如果不對(duì)嵌套深度進(jìn)行限制,就會(huì)被攻擊者利用進(jìn)行拒絕服務(wù)攻擊。

@圖南:

在開(kāi)發(fā)中,我們可能經(jīng)常會(huì)遇到這樣的需求:

1. 查詢所有文章,返回內(nèi)容中包含作者信息

2. 查詢作者信息,返回內(nèi)容中包含此作者寫(xiě)的所有文章

當(dāng)然,在我們開(kāi)發(fā)的前端中這兩個(gè)接口一定是單獨(dú)使用的,但攻擊者可以利用這它們的包含關(guān)系進(jìn)行嵌套查詢。

如下面例子,我們定義了Blog和Author:

    type Blog {        _id: String!        type: BlogType        avatar: String        title: String        content: [String]        author: Author        # ...    }    type Author {        _id: String!        name: String        blog: [Blog]    }

構(gòu)建各自的Query:

    extend type Query {        blogs(            blogId: ID            systemType: String!        ): [Blog]    }    extend type Query {        author(            _id: String!        ): Author    }

我們可以構(gòu)造如下的查詢,此查詢可無(wú)限循環(huán)下去,就有可能造成拒絕服務(wù)攻擊:

  query GetBlogs($blogId: ID, $systemType: String!) {        blogs(blogId: $blogId, systemType: $systemType) {            _id            title            type            content            author {                name                blog {                    author {                        name                        blog {                            author {                                name                                blog {                                    author {                                        name                                        blog {                                            author {                                                name                                                blog {                                                    author {                                                        name                                                        blog {                                                            author {                                                                name                                                                blog {                                                                    author {                                                                           name                                                                           # and so on...                                                                    }                                                                }                                                            }                                                        }                                                    }                                                }                                            }                                        }                                    }                                }                            }                        }                    }                    title                    createdAt                    publishedAt                }            }            publishedAt        }    }

避免此問(wèn)題我們需要在GraphQL服務(wù)器上限制查詢深度,同時(shí)在設(shè)計(jì)GraphQL接口時(shí)應(yīng)盡量避免出現(xiàn)此類(lèi)問(wèn)題。仍然以Node.js為例,graphql-depth-limit就可以解決這樣的問(wèn)題。

    // ...    import depthLimit from 'graphql-depth-limit';    // ...    const server = new ApolloServer({        typeDefs: schemaText,        resolvers: resolverMap,        context: ({ ctx }) => {            const token = ctx.req.headers.authorization || '';            const user = getUser(token);            console.log('user',user)            return {                ...user,                ...ctx,                ...app.context            };        },        validationRules: [ depthLimit(10) ]    });// ...

添加限制后,請(qǐng)求深度過(guò)大時(shí)會(huì)看到如下報(bào)錯(cuò)信息:

如何進(jìn)行GraphQL的分析

它只是個(gè)接口

@gyyyy:

作為Web API的一員,GraphQL和RESTful API一樣,有可能被攻擊者通過(guò)對(duì)參數(shù)注入惡意數(shù)據(jù)影響到后端應(yīng)用,產(chǎn)生XSS、SQL注入、RCE等安全問(wèn)題。此外,上文也提到了很多GraphQL的特性,一些特殊場(chǎng)景下,這些特性會(huì)被攻擊者利用來(lái)優(yōu)化攻擊流程甚至增強(qiáng)攻擊效果。比如之前說(shuō)的內(nèi)省機(jī)制和默認(rèn)開(kāi)啟的GraphiQL調(diào)試工具等,還有它同時(shí)支持GET和POST兩種請(qǐng)求方法,對(duì)于CSRF這些漏洞的利用會(huì)提供更多的便利。

當(dāng)然,有些特性也提供了部分保護(hù)能力,不過(guò)只是『部分』而已。

@圖南:

GraphQL的類(lèi)型系統(tǒng)對(duì)注入是一層天然屏障,但是如果開(kāi)發(fā)者的處理方式不正確,仍然會(huì)有例外。

比如下面的例子,參數(shù)類(lèi)型是字符串:

    query GetAllUsers($filter: String!) {        users(filter: $filter) {            _id            username            email        }    }

假如后端沒(méi)有對(duì)filter的值進(jìn)行任何安全性校驗(yàn),直接查詢數(shù)據(jù)庫(kù),傳入一段SQL語(yǔ)句字符串,可能構(gòu)成SQL注入:

    {"filter": "' or ''='"}

或者JSON字符串構(gòu)成NoSQL注入:

    {"filter": "{\"$ne\": null}"}

GraphQL真的只是一個(gè)API技術(shù),它為API連接的前后端提供了一種新的便捷處理方案。無(wú)論如何,該做鑒權(quán)的就鑒權(quán),該校驗(yàn)數(shù)據(jù)的還是一定得校驗(yàn)。

關(guān)于如何進(jìn)行GraphQL的分析就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI