Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
1d7b8768
authored
Jun 07, 2026
by
Archer
Committed by
GitHub
Jun 07, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: guard OpenAPI schema refs during HTTP tool import (#7073)
parent
54a53e7d
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
246 additions
and
18 deletions
+246
-18
packages/global/common/string/swagger.ts
+28
-3
packages/global/core/app/jsonschema.ts
+7
-8
packages/global/test/core/app/jsonschema.test.ts
+93
-0
projects/app/src/pages/api/core/app/httpTools/getApiSchemaByUrl.ts
+18
-7
projects/app/test/api/core/app/httpTools/getApiSchemaByUrl.test.ts
+100
-0
No files found.
packages/global/common/string/swagger.ts
View file @
1d7b8768
/* v8 ignore file */
import
SwaggerParser
from
'@apidevtools/swagger-parser'
;
import
SwaggerParser
from
'@apidevtools/swagger-parser'
;
import
yaml
from
'js-yaml'
;
export
const
loadOpenAPISchemaFromUrl
=
async
(
url
:
string
)
=>
{
/**
return
SwaggerParser
.
bundle
(
url
);
* 解析用户导入的 OpenAPI 文档字符串。
*
* URL 导入场景会先由服务端受控下载文本,再交给该函数解析,避免把 URL
* 直接传给 SwaggerParser 触发其内置远程 resolver。
*/
export
const
parseOpenAPISchemaString
=
(
schemaText
:
string
)
=>
{
try
{
return
JSON
.
parse
(
schemaText
);
}
catch
{
return
yaml
.
load
(
schemaText
,
{
schema
:
yaml
.
FAILSAFE_SCHEMA
});
}
};
/**
* 仅对已取得的 OpenAPI 对象做本地 bundle。
*
* 禁止 SwaggerParser 的 file/http resolver,防止 OpenAPI URL 导入后继续解析
* 远程 `$ref`,绕过服务端 SSRF 校验。
*/
export
const
bundleOpenAPISchema
=
async
(
schema
:
unknown
)
=>
{
return
SwaggerParser
.
bundle
(
schema
as
any
,
{
resolve
:
{
file
:
false
,
http
:
false
}
});
};
};
packages/global/core/app/jsonschema.ts
View file @
1d7b8768
...
@@ -7,11 +7,11 @@ import {
...
@@ -7,11 +7,11 @@ import {
type
FlowNodeOutputItemType
type
FlowNodeOutputItemType
}
from
'../workflow/type/io'
;
}
from
'../workflow/type/io'
;
import
SwaggerParser
from
'@apidevtools/swagger-parser'
;
import
SwaggerParser
from
'@apidevtools/swagger-parser'
;
import
yaml
from
'js-yaml'
;
import
type
{
OpenAPIV3
}
from
'openapi-types'
;
import
type
{
OpenAPIV3
}
from
'openapi-types'
;
import
type
{
OpenApiJsonSchema
}
from
'./tool/httpTool/type'
;
import
type
{
OpenApiJsonSchema
}
from
'./tool/httpTool/type'
;
import
{
i18nT
}
from
'../../common/i18n/utils'
;
import
{
i18nT
}
from
'../../common/i18n/utils'
;
import
z
from
'zod'
;
import
z
from
'zod'
;
import
{
parseOpenAPISchemaString
}
from
'../../common/string/swagger'
;
export
const
JsonSchemaPropertiesItemSchema
=
z
.
object
({
export
const
JsonSchemaPropertiesItemSchema
=
z
.
object
({
// 基本类型定义
// 基本类型定义
...
@@ -193,14 +193,13 @@ export const jsonSchema2NodeOutput = ({
...
@@ -193,14 +193,13 @@ export const jsonSchema2NodeOutput = ({
export
const
str2OpenApiSchema
=
async
(
yamlStr
=
''
):
Promise
<
OpenApiJsonSchema
>
=>
{
export
const
str2OpenApiSchema
=
async
(
yamlStr
=
''
):
Promise
<
OpenApiJsonSchema
>
=>
{
try
{
try
{
const
data
=
(()
=>
{
const
data
=
parseOpenAPISchemaString
(
yamlStr
);
try
{
const
jsonSchema
=
(
await
SwaggerParser
.
dereference
(
data
,
{
return
JSON
.
parse
(
yamlStr
);
resolve
:
{
}
catch
(
jsonError
)
{
file
:
false
,
return
yaml
.
load
(
yamlStr
,
{
schema
:
yaml
.
FAILSAFE_SCHEMA
});
http
:
false
}
}
})();
}))
as
OpenAPIV3
.
Document
;
const
jsonSchema
=
(
await
SwaggerParser
.
dereference
(
data
))
as
OpenAPIV3
.
Document
;
const
serverPath
=
(()
=>
{
const
serverPath
=
(()
=>
{
if
(
jsonSchema
.
servers
&&
jsonSchema
.
servers
.
length
>
0
)
{
if
(
jsonSchema
.
servers
&&
jsonSchema
.
servers
.
length
>
0
)
{
...
...
packages/global/test/core/app/jsonschema.test.ts
View file @
1d7b8768
...
@@ -10,6 +10,7 @@ import {
...
@@ -10,6 +10,7 @@ import {
getSchemaValueType
,
getSchemaValueType
,
str2OpenApiSchema
str2OpenApiSchema
}
from
'@fastgpt/global/core/app/jsonschema'
;
}
from
'@fastgpt/global/core/app/jsonschema'
;
import
{
bundleOpenAPISchema
}
from
'@fastgpt/global/common/string/swagger'
;
import
{
WorkflowIOValueTypeEnum
}
from
'@fastgpt/global/core/workflow/constants'
;
import
{
WorkflowIOValueTypeEnum
}
from
'@fastgpt/global/core/workflow/constants'
;
import
{
FlowNodeInputItemTypeSchema
}
from
'@fastgpt/global/core/workflow/type/io'
;
import
{
FlowNodeInputItemTypeSchema
}
from
'@fastgpt/global/core/workflow/type/io'
;
...
@@ -572,6 +573,63 @@ describe('getSchemaValueType', () => {
...
@@ -572,6 +573,63 @@ describe('getSchemaValueType', () => {
});
});
describe
(
'str2OpenApiSchema'
,
()
=>
{
describe
(
'str2OpenApiSchema'
,
()
=>
{
it
(
'should dereference local refs without resolving remote refs'
,
async
()
=>
{
const
openApiJson
=
JSON
.
stringify
({
openapi
:
'3.0.0'
,
info
:
{
title
:
'Test API'
,
version
:
'1.0.0'
},
paths
:
{
'/users'
:
{
post
:
{
operationId
:
'createUser'
,
requestBody
:
{
content
:
{
'application/json'
:
{
schema
:
{
$ref
:
'#/components/schemas/User'
}
}
}
},
responses
:
{
'200'
:
{
description
:
'OK'
}
}
}
}
},
components
:
{
schemas
:
{
User
:
{
type
:
'object'
,
properties
:
{
name
:
{
type
:
'string'
}
}
}
}
}
});
const
result
=
await
str2OpenApiSchema
(
openApiJson
);
expect
(
result
.
pathData
[
0
].
request
.
content
[
'application/json'
].
schema
.
properties
.
name
).
toEqual
({
type
:
'string'
});
});
it
(
'should reject remote refs without resolving them'
,
async
()
=>
{
const
openApiJson
=
JSON
.
stringify
({
openapi
:
'3.0.0'
,
info
:
{
title
:
'Test API'
,
version
:
'1.0.0'
},
paths
:
{},
components
:
{
schemas
:
{
Leak
:
{
$ref
:
'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
}
}
}
});
await
expect
(
str2OpenApiSchema
(
openApiJson
)).
rejects
.
toBe
(
'common:plugin.Invalid Schema'
);
});
it
(
'should parse valid OpenAPI 3.0 JSON schema'
,
async
()
=>
{
it
(
'should parse valid OpenAPI 3.0 JSON schema'
,
async
()
=>
{
const
openApiJson
=
JSON
.
stringify
({
const
openApiJson
=
JSON
.
stringify
({
openapi
:
'3.0.0'
,
openapi
:
'3.0.0'
,
...
@@ -891,3 +949,38 @@ paths:
...
@@ -891,3 +949,38 @@ paths:
expect
(
result
.
pathData
[
0
].
params
).
toHaveLength
(
2
);
expect
(
result
.
pathData
[
0
].
params
).
toHaveLength
(
2
);
});
});
});
});
describe
(
'bundleOpenAPISchema'
,
()
=>
{
it
(
'should bundle local refs from an already loaded schema object'
,
async
()
=>
{
const
result
=
await
bundleOpenAPISchema
({
openapi
:
'3.0.0'
,
info
:
{
title
:
'Test API'
,
version
:
'1.0.0'
},
paths
:
{},
components
:
{
schemas
:
{
User
:
{
type
:
'object'
,
properties
:
{
name
:
{
type
:
'string'
}
}
},
Payload
:
{
$ref
:
'#/components/schemas/User'
}
}
}
});
expect
(
result
.
components
.
schemas
.
Payload
).
toEqual
({
$ref
:
'#/components/schemas/User'
});
});
it
(
'should reject remote refs when bundling an already loaded schema object'
,
async
()
=>
{
await
expect
(
bundleOpenAPISchema
({
openapi
:
'3.0.0'
,
info
:
{
title
:
'Test API'
,
version
:
'1.0.0'
},
paths
:
{},
components
:
{
schemas
:
{
Leak
:
{
$ref
:
'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
}
}
}
})
).
rejects
.
toThrow
(
'Unable to resolve $ref pointer'
);
});
});
projects/app/src/pages/api/core/app/httpTools/getApiSchemaByUrl.ts
View file @
1d7b8768
import
{
loadOpenAPISchemaFromUrl
}
from
'@fastgpt/global/common/string/swagger'
;
import
{
bundleOpenAPISchema
,
parseOpenAPISchemaString
}
from
'@fastgpt/global/common/string/swagger'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
{
NextAPI
}
from
'@/service/middleware/entry'
;
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
{
isInternalAddress
}
from
'@fastgpt/service/common/system/utils'
;
import
{
axios
}
from
'@fastgpt/service/common/api/axios'
;
import
{
checkUrlSafety
}
from
'@fastgpt/service/common/system/utils'
;
import
{
authCert
}
from
'@fastgpt/service/support/permission/auth/common'
;
import
{
authCert
}
from
'@fastgpt/service/support/permission/auth/common'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
parseApiInput
}
from
'@fastgpt/service/common/zod/requestParseError'
;
import
{
import
{
...
@@ -11,7 +15,7 @@ import {
...
@@ -11,7 +15,7 @@ import {
type
GetApiSchemaByUrlResponseType
type
GetApiSchemaByUrlResponseType
}
from
'@fastgpt/global/openapi/core/app/httpTools/api'
;
}
from
'@fastgpt/global/openapi/core/app/httpTools/api'
;
async
function
handler
(
export
async
function
handler
(
req
:
ApiRequestProps
<
GetApiSchemaByUrlBodyType
>
req
:
ApiRequestProps
<
GetApiSchemaByUrlBodyType
>
):
Promise
<
GetApiSchemaByUrlResponseType
>
{
):
Promise
<
GetApiSchemaByUrlResponseType
>
{
const
{
const
{
...
@@ -23,11 +27,18 @@ async function handler(
...
@@ -23,11 +27,18 @@ async function handler(
await
authCert
({
req
,
authToken
:
true
});
await
authCert
({
req
,
authToken
:
true
});
if
(
await
isInternalAddress
(
url
))
{
await
checkUrlSafety
(
url
,
'OpenAPI Schema URL'
);
return
Promise
.
reject
(
'Invalid url'
);
}
const
{
data
}
=
await
axios
.
get
<
string
>
(
url
,
{
responseType
:
'text'
,
maxRedirects
:
0
,
timeout
:
30000
,
transformResponse
:
(
value
)
=>
value
});
return
GetApiSchemaByUrlResponseSchema
.
parse
(
await
loadOpenAPISchemaFromUrl
(
url
));
return
GetApiSchemaByUrlResponseSchema
.
parse
(
await
bundleOpenAPISchema
(
parseOpenAPISchemaString
(
data
))
);
}
}
export
default
NextAPI
(
handler
);
export
default
NextAPI
(
handler
);
projects/app/test/api/core/app/httpTools/getApiSchemaByUrl.test.ts
0 → 100644
View file @
1d7b8768
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
;
import
type
{
ApiRequestProps
}
from
'@fastgpt/service/type/next'
;
import
type
{
GetApiSchemaByUrlBodyType
}
from
'@fastgpt/global/openapi/core/app/httpTools/api'
;
const
mocks
=
vi
.
hoisted
(()
=>
({
authCert
:
vi
.
fn
(),
checkUrlSafety
:
vi
.
fn
(),
axiosGet
:
vi
.
fn
()
}));
vi
.
mock
(
'@/service/middleware/entry'
,
()
=>
({
NextAPI
:
(
handler
:
any
)
=>
handler
}));
vi
.
mock
(
'@fastgpt/service/support/permission/auth/common'
,
()
=>
({
authCert
:
mocks
.
authCert
}));
vi
.
mock
(
'@fastgpt/service/common/system/utils'
,
()
=>
({
checkUrlSafety
:
mocks
.
checkUrlSafety
}));
vi
.
mock
(
'@fastgpt/service/common/api/axios'
,
()
=>
({
axios
:
{
get
:
mocks
.
axiosGet
}
}));
import
{
handler
}
from
'@/pages/api/core/app/httpTools/getApiSchemaByUrl'
;
describe
(
'getApiSchemaByUrl handler'
,
()
=>
{
beforeEach
(()
=>
{
vi
.
clearAllMocks
();
mocks
.
authCert
.
mockResolvedValue
({
teamId
:
'team-1'
,
tmbId
:
'tmb-1'
});
mocks
.
checkUrlSafety
.
mockResolvedValue
(
undefined
);
});
it
(
'downloads schema through guarded axios before bundling'
,
async
()
=>
{
mocks
.
axiosGet
.
mockResolvedValue
({
data
:
JSON
.
stringify
({
openapi
:
'3.0.0'
,
info
:
{
title
:
'Test API'
,
version
:
'1.0.0'
},
paths
:
{
'/users'
:
{
get
:
{
operationId
:
'getUsers'
,
responses
:
{
'200'
:
{
description
:
'OK'
}
}
}
}
}
})
});
const
result
=
await
handler
({
body
:
{
url
:
'https://example.com/openapi.json'
}
}
as
ApiRequestProps
<
GetApiSchemaByUrlBodyType
>
);
expect
(
mocks
.
authCert
).
toHaveBeenCalledWith
({
req
:
expect
.
any
(
Object
),
authToken
:
true
});
expect
(
mocks
.
checkUrlSafety
).
toHaveBeenCalledWith
(
'https://example.com/openapi.json'
,
'OpenAPI Schema URL'
);
expect
(
mocks
.
axiosGet
).
toHaveBeenCalledWith
(
'https://example.com/openapi.json'
,
expect
.
objectContaining
({
responseType
:
'text'
,
maxRedirects
:
0
,
timeout
:
30000
})
);
expect
(
result
.
paths
[
'/users'
]).
toBeDefined
();
});
it
(
'rejects external refs from downloaded schemas'
,
async
()
=>
{
mocks
.
axiosGet
.
mockResolvedValue
({
data
:
JSON
.
stringify
({
openapi
:
'3.0.0'
,
info
:
{
title
:
'Test API'
,
version
:
'1.0.0'
},
paths
:
{},
components
:
{
schemas
:
{
Leak
:
{
$ref
:
'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
}
}
}
})
});
await
expect
(
handler
({
body
:
{
url
:
'https://example.com/openapi.json'
}
}
as
ApiRequestProps
<
GetApiSchemaByUrlBodyType
>
)
).
rejects
.
toThrow
(
'Unable to resolve $ref pointer'
);
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment