-
Notifications
You must be signed in to change notification settings - Fork 90
feat: validate generic package asset labels to prevent 403 Forbidden errors #911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
0e35c06
24023a7
b90cf02
0929b19
fb9c774
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,33 @@ export default async (pluginConfig, context) => { | |
.filter(([option, value]) => !isValid(option, value)) | ||
.map(([option, value]) => getError(`EINVALID${option.toUpperCase()}`, { [option]: value })); | ||
|
||
// Validate generic package labels | ||
if (options.assets && Array.isArray(options.assets)) { | ||
const isValidGenericPackageLabel = (label) => { | ||
// GitLab generic package filename restrictions | ||
// Can contain: A-Z, a-z, 0-9, . _ - + ~ @ / | ||
// Cannot start with: ~ or @ | ||
// Cannot end with: ~ or @ | ||
// Cannot contain spaces | ||
if (!label) return true; // label is optional | ||
if (typeof label !== "string") return false; | ||
if (/\s/.test(label)) return false; // no spaces | ||
if (/^[~@]/.test(label)) return false; // cannot start with ~ or @ | ||
if (/[~@]$/.test(label)) return false; // cannot end with ~ or @ | ||
// Check if it only contains allowed characters | ||
if (!/^[A-Za-z0-9._\-+~@/]+$/.test(label)) return false; | ||
return true; | ||
}; | ||
|
||
options.assets.forEach((asset) => { | ||
if (isPlainObject(asset) && asset.target === "generic_package" && asset.label) { | ||
if (!isValidGenericPackageLabel(asset.label)) { | ||
errors.push(getError("EINVALIDGENERICPACKAGELABEL", { label: asset.label, asset })); | ||
Comment on lines
+67
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot This can go in line 36 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand what should go in line 36. The validation check at lines 67-69 needs to produce specific |
||
} | ||
} | ||
}); | ||
} | ||
|
||
if (!projectPath) { | ||
errors.push(getError("EINVALIDGITLABURL")); | ||
} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -988,3 +988,193 @@ test.serial( | |
t.true(gitlab.isDone()); | ||
} | ||
); | ||
|
||
test.serial("Throw SemanticReleaseError if generic package asset label contains spaces", async (t) => { | ||
|
||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "Style package", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
const { | ||
errors: [error], | ||
} = await t.throwsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.is(error.name, "SemanticReleaseError"); | ||
t.is(error.code, "EINVALIDGENERICPACKAGELABEL"); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Throw SemanticReleaseError if generic package asset label starts with ~", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "~invalid", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
const { | ||
errors: [error], | ||
} = await t.throwsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.is(error.name, "SemanticReleaseError"); | ||
t.is(error.code, "EINVALIDGENERICPACKAGELABEL"); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Throw SemanticReleaseError if generic package asset label starts with @", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "@invalid", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
const { | ||
errors: [error], | ||
} = await t.throwsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.is(error.name, "SemanticReleaseError"); | ||
t.is(error.code, "EINVALIDGENERICPACKAGELABEL"); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Throw SemanticReleaseError if generic package asset label ends with ~", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "invalid~", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
const { | ||
errors: [error], | ||
} = await t.throwsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.is(error.name, "SemanticReleaseError"); | ||
t.is(error.code, "EINVALIDGENERICPACKAGELABEL"); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Throw SemanticReleaseError if generic package asset label ends with @", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "invalid@", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
const { | ||
errors: [error], | ||
} = await t.throwsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.is(error.name, "SemanticReleaseError"); | ||
t.is(error.code, "EINVALIDGENERICPACKAGELABEL"); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Throw SemanticReleaseError if generic package asset label contains invalid characters", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "invalid$char", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
const { | ||
errors: [error], | ||
} = await t.throwsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.is(error.name, "SemanticReleaseError"); | ||
t.is(error.code, "EINVALIDGENERICPACKAGELABEL"); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Does not throw for valid generic package asset labels", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [ | ||
{ path: "file.css", label: "valid-label_123.tar.gz", target: "generic_package" }, | ||
{ path: "file2.css", label: "another.valid+label~0", target: "generic_package" }, | ||
{ path: "file3.css", label: "path/to/file", target: "generic_package" }, | ||
]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
await t.notThrowsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Does not throw for generic package assets without labels", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", target: "generic_package" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
await t.notThrowsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.true(gitlab.isDone()); | ||
}); | ||
|
||
test.serial("Does not throw for non-generic package assets with spaces in labels", async (t) => { | ||
const owner = "test_user"; | ||
const repo = "test_repo"; | ||
const env = { GITLAB_TOKEN: "gitlab_token" }; | ||
const assets = [{ path: "file.css", label: "Valid Label With Spaces" }]; | ||
const gitlab = authenticate(env) | ||
.get(`/projects/${owner}%2F${repo}`) | ||
.reply(200, { permissions: { project_access: { access_level: 40 } } }); | ||
|
||
await t.notThrowsAsync( | ||
verify( | ||
{ assets }, | ||
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger } | ||
) | ||
); | ||
t.true(gitlab.isDone()); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot Move this check to the existing VALIDATORS object above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved
isValidGenericPackageLabel
to the top-level scope above the VALIDATORS object in commit b7e8f14.