Skip to content

Commit b85253f

Browse files
authored
feat(plugin): implement unauthorized Langgenius plugin blocking (#458)
* feat(plugin): implement unauthorized Langgenius plugin blocking - Added configuration option to disable installation of plugins falsely claiming Langgenius authorship. - Introduced new error handling for unauthorized Langgenius claims during plugin installation. - Implemented tests to validate unauthorized Langgenius detection logic. - Updated environment configuration and service files to support the new feature. * fix: typo * refactor(plugin): update Langgenius plugin signature enforcement - Renamed configuration option from DISABLE_UNAUTHORIZED_LANGGENIUS_PACKAGE to ENFORCE_LANGGENIUS_PLUGIN_SIGNATURES for clarity. - Updated error messages and logic in the plugin installation process to reflect the new configuration. - Enhanced tests to validate the behavior of unauthorized Langgenius plugin detection with the new enforcement setting.
1 parent 6473831 commit b85253f

File tree

7 files changed

+268
-1
lines changed

7 files changed

+268
-1
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ THIRD_PARTY_SIGNATURE_VERIFICATION_ENABLED=false
140140
# A comma-separated list of file paths to public keys in addition to the official public key for signature verification
141141
THIRD_PARTY_SIGNATURE_VERIFICATION_PUBLIC_KEYS=
142142

143+
# Enforce signature verification for plugins claiming Langgenius authorship
144+
# Set to "false" to allow installation of unsigned plugins claiming to be from Langgenius (security risk)
145+
# Community and partner plugins that don't claim Langgenius authorship are not affected
146+
ENFORCE_LANGGENIUS_PLUGIN_SIGNATURES=true
147+
143148
# proxy settings, example: HTTP_PROXY=http://host.docker.internal:7890
144149
HTTP_PROXY=
145150
HTTPS_PROXY=

internal/service/exec.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package service
2+
3+
import "errors"
4+
5+
var (
6+
ErrUnauthorizedLanggenius = errors.New(`
7+
plugin installation blocked: this plugin claims to be from Langgenius but lacks official signature verification.
8+
Set ENFORCE_LANGGENIUS_PLUGIN_SIGNATURES=false to allow installation (not recommended)`,
9+
)
10+
)

internal/service/install_plugin.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,13 @@ func doInstallPluginRuntime(
131131
return
132132
}
133133

134-
zipDecoder, err = decoder.NewZipPluginDecoder(pkgFile)
134+
zipDecoder, err = decoder.NewZipPluginDecoderWithThirdPartySignatureVerificationConfig(
135+
pkgFile,
136+
&decoder.ThirdPartySignatureVerificationConfig{
137+
Enabled: config.ThirdPartySignatureVerificationEnabled,
138+
PublicKeyPaths: config.ThirdPartySignatureVerificationPublicKeys,
139+
},
140+
)
135141
if err != nil {
136142
updateTaskStatus(func(task *models.InstallTask, plugin *models.InstallTaskPluginStatus) {
137143
task.Status = models.InstallTaskStatusFailed

internal/service/plugin_decoder.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ func UploadPluginPkg(
6666
verification = decoder.DefaultVerification()
6767
}
6868

69+
if config.EnforceLanggeniusSignatures {
70+
if isUnauthorizedLanggenius(declaration, verification) {
71+
return exception.BadRequestError(ErrUnauthorizedLanggenius).ToResponse()
72+
}
73+
}
74+
6975
return entities.NewSuccessResponse(map[string]any{
7076
"unique_identifier": pluginUniqueIdentifier,
7177
"manifest": declaration,
@@ -158,6 +164,17 @@ func UploadPluginBundle(
158164
}
159165
}
160166

167+
verification, _ := decoderInstance.Verification()
168+
if verification == nil && decoderInstance.Verified() {
169+
verification = decoder.DefaultVerification()
170+
}
171+
172+
if config.EnforceLanggeniusSignatures {
173+
if isUnauthorizedLanggenius(declaration, verification) {
174+
return exception.BadRequestError(ErrUnauthorizedLanggenius).ToResponse()
175+
}
176+
}
177+
161178
result = append(result, map[string]any{
162179
"type": "package",
163180
"value": map[string]any{
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package service
2+
3+
import (
4+
"strings"
5+
6+
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
7+
"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder"
8+
)
9+
10+
// isUnauthorizedLanggenius checks if a plugin falsely claims to be from Langgenius
11+
func isUnauthorizedLanggenius(declaration *plugin_entities.PluginDeclaration, verification *decoder.Verification) bool {
12+
// Check if plugin claims to be from Langgenius (case-insensitive)
13+
claimsLanggenius := strings.ToLower(declaration.Author) == string(decoder.AUTHORIZED_CATEGORY_LANGGENIUS)
14+
15+
// If claims Langgenius but not properly authorized
16+
if claimsLanggenius {
17+
return verification == nil || // if no verification, it's unauthorized
18+
verification.AuthorizedCategory != decoder.AUTHORIZED_CATEGORY_LANGGENIUS
19+
}
20+
21+
// Non-Langgenius plugins are allowed
22+
return false
23+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package service
2+
3+
import (
4+
"testing"
5+
6+
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
7+
"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder"
8+
)
9+
10+
func TestIsUnauthorizedLanggenius(t *testing.T) {
11+
// Tests for isUnauthorizedLanggenius function
12+
// This function is used when ENFORCE_LANGGENIUS_PLUGIN_SIGNATURES=true (default)
13+
// to prevent unauthorized plugins from impersonating Langgenius
14+
tests := []struct {
15+
name string
16+
author string
17+
verification *decoder.Verification
18+
want bool
19+
}{
20+
{
21+
name: "langgenius author with proper verification",
22+
author: "langgenius",
23+
verification: &decoder.Verification{
24+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS,
25+
},
26+
want: false, // properly authorized
27+
},
28+
{
29+
name: "langgenius author with partner verification",
30+
author: "langgenius",
31+
verification: &decoder.Verification{
32+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_PARTNER,
33+
},
34+
want: true, // unauthorized - claims langgenius but verified as partner
35+
},
36+
{
37+
name: "langgenius author with community verification",
38+
author: "langgenius",
39+
verification: &decoder.Verification{
40+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_COMMUNITY,
41+
},
42+
want: true, // unauthorized - claims langgenius but verified as community
43+
},
44+
{
45+
name: "langgenius author without verification",
46+
author: "langgenius",
47+
verification: nil,
48+
want: true, // unauthorized - claims langgenius but no verification
49+
},
50+
{
51+
name: "Langgenius author (capital L) with proper verification",
52+
author: "Langgenius",
53+
verification: &decoder.Verification{
54+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS,
55+
},
56+
want: false, // properly authorized (case-insensitive)
57+
},
58+
{
59+
name: "LANGGENIUS author (all caps) with proper verification",
60+
author: "LANGGENIUS",
61+
verification: &decoder.Verification{
62+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS,
63+
},
64+
want: false, // properly authorized (case-insensitive)
65+
},
66+
{
67+
name: "LANGGENIUS author (all caps) without verification",
68+
author: "LANGGENIUS",
69+
verification: nil,
70+
want: true, // unauthorized - claims langgenius but no verification
71+
},
72+
{
73+
name: "community author with community verification",
74+
author: "community_developer",
75+
verification: &decoder.Verification{
76+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_COMMUNITY,
77+
},
78+
want: false, // authorized - doesn't claim langgenius
79+
},
80+
{
81+
name: "partner author with partner verification",
82+
author: "partner_company",
83+
verification: &decoder.Verification{
84+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_PARTNER,
85+
},
86+
want: false, // authorized - doesn't claim langgenius
87+
},
88+
{
89+
name: "community author without verification",
90+
author: "john_doe",
91+
verification: nil,
92+
want: false, // allowed - doesn't claim langgenius
93+
},
94+
{
95+
name: "empty author with langgenius verification",
96+
author: "",
97+
verification: &decoder.Verification{
98+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS,
99+
},
100+
want: false, // allowed - doesn't claim langgenius
101+
},
102+
{
103+
name: "empty author without verification",
104+
author: "",
105+
verification: nil,
106+
want: false, // allowed - doesn't claim langgenius
107+
},
108+
{
109+
name: "author contains langgenius but not exact match",
110+
author: "not_langgenius",
111+
verification: &decoder.Verification{
112+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_COMMUNITY,
113+
},
114+
want: false, // allowed - not exact match
115+
},
116+
{
117+
name: "author langgenius_team",
118+
author: "langgenius_team",
119+
verification: &decoder.Verification{
120+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_COMMUNITY,
121+
},
122+
want: false, // allowed - not exact match
123+
},
124+
{
125+
name: "author my_langgenius",
126+
author: "my_langgenius",
127+
verification: &decoder.Verification{
128+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_COMMUNITY,
129+
},
130+
want: false, // allowed - not exact match
131+
},
132+
}
133+
134+
for _, tt := range tests {
135+
t.Run(tt.name, func(t *testing.T) {
136+
declaration := &plugin_entities.PluginDeclaration{
137+
PluginDeclarationWithoutAdvancedFields: plugin_entities.PluginDeclarationWithoutAdvancedFields{
138+
Author: tt.author,
139+
},
140+
}
141+
142+
got := isUnauthorizedLanggenius(declaration, tt.verification)
143+
if got != tt.want {
144+
t.Errorf("isUnauthorizedLanggenius() = %v, want %v", got, tt.want)
145+
}
146+
})
147+
}
148+
}
149+
150+
func TestIsUnauthorizedLanggenius_EdgeCases(t *testing.T) {
151+
tests := []struct {
152+
name string
153+
author string
154+
verification *decoder.Verification
155+
want bool
156+
}{
157+
{
158+
name: "langgenius with spaces",
159+
author: " langgenius ",
160+
verification: &decoder.Verification{
161+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS,
162+
},
163+
want: false, // spaces don't affect the comparison after lowercase
164+
},
165+
{
166+
name: "langgenius with spaces but no verification",
167+
author: " langgenius ",
168+
verification: nil,
169+
want: false, // with spaces, not exact match after lowercase
170+
},
171+
{
172+
name: "LaNgGeNiUs mixed case",
173+
author: "LaNgGeNiUs",
174+
verification: &decoder.Verification{
175+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS,
176+
},
177+
want: false, // properly authorized (case-insensitive)
178+
},
179+
{
180+
name: "langgenius. with punctuation",
181+
author: "langgenius.",
182+
verification: &decoder.Verification{
183+
AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_COMMUNITY,
184+
},
185+
want: false, // not exact match due to punctuation
186+
},
187+
}
188+
189+
for _, tt := range tests {
190+
t.Run(tt.name, func(t *testing.T) {
191+
declaration := &plugin_entities.PluginDeclaration{
192+
PluginDeclarationWithoutAdvancedFields: plugin_entities.PluginDeclarationWithoutAdvancedFields{
193+
Author: tt.author,
194+
},
195+
}
196+
197+
got := isUnauthorizedLanggenius(declaration, tt.verification)
198+
if got != tt.want {
199+
t.Errorf("isUnauthorizedLanggenius() = %v, want %v for author=%q", got, tt.want, tt.author)
200+
}
201+
})
202+
}
203+
}

internal/types/app/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ type Config struct {
146146
// a comma-separated list of file paths to public keys in addition to the official public key for signature verification
147147
ThirdPartySignatureVerificationPublicKeys []string `envconfig:"THIRD_PARTY_SIGNATURE_VERIFICATION_PUBLIC_KEYS" default:""`
148148

149+
// Enforce signature verification for plugins claiming Langgenius authorship
150+
EnforceLanggeniusSignatures bool `envconfig:"ENFORCE_LANGGENIUS_PLUGIN_SIGNATURES" default:"true"`
151+
149152
// lifetime state management
150153
LifetimeCollectionHeartbeatInterval int `envconfig:"LIFETIME_COLLECTION_HEARTBEAT_INTERVAL" validate:"required"`
151154
LifetimeCollectionGCInterval int `envconfig:"LIFETIME_COLLECTION_GC_INTERVAL" validate:"required"`

0 commit comments

Comments
 (0)