fix(gitlab): reject invalid project identifiers in config validation (#8667)

The project-field validation regex was only end-anchored, so display
names containing spaces (e.g. "My Group/My Gitlab") passed validation
and produced a confusing 404 at poll time instead of inline feedback
(#8665). Anchor the regex at both ends and simplify the path branch to a
plain character class so it stays a permissive "did you paste a display
name?" guard (still accepts consecutive hyphens and other GitLab-valid
paths). Clarify the field hint to ask for the path slug or numeric ID,
and add regression tests plus a field-wiring check.
This commit is contained in:
Johannes Millan 2026-07-02 10:43:09 +02:00 committed by GitHub
parent 785911c232
commit 3d952fb157
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 71 additions and 2 deletions

View file

@ -0,0 +1,62 @@
import {
GITLAB_CONFIG_FORM_SECTION,
GITLAB_PROJECT_REGEX,
} from './gitlab-cfg-form.const';
describe('GITLAB_PROJECT_REGEX', () => {
let projectPattern: RegExp;
beforeAll(() => {
// Verify the form field is actually wired to the exported regex, so this
// spec fails if someone swaps the field's `pattern` out from under it.
const projectField = GITLAB_CONFIG_FORM_SECTION.items!.find(
(item) => item.key === 'project',
);
const pattern = projectField?.templateOptions?.pattern as RegExp;
expect(pattern).toBe(GITLAB_PROJECT_REGEX);
projectPattern = pattern;
});
// Angular's Validators.pattern uses `regex.test(value)` as-is for a RegExp
// (no auto-anchoring), so the regex itself must be anchored at both ends.
const isValid = (value: string): boolean => projectPattern.test(value);
describe('valid project identifiers', () => {
const validCases = [
'super-productivity/super-productivity',
'group/subgroup/project',
'my_group/my.project',
'a.b-c/d_e',
'group%2Fproject',
'group%2Fsub%2Fproject',
// GitLab allows consecutive hyphens in a path segment; must not be rejected.
'foo--bar',
'single',
'12345',
];
validCases.forEach((value) => {
it(`accepts "${value}"`, () => {
expect(isValid(value)).toBe(true);
});
});
});
describe('invalid project identifiers', () => {
// Regression for #8665: a display name with a space must be rejected inline
// instead of producing a confusing 404 at poll time.
const invalidCases = [
'My Group/My Gitlab',
'group/Test Gitlab',
'has space',
' leadingSpace',
'trailingSpace ',
'https://gitlab.com/foo/bar',
'foo/bar?scope=all',
];
invalidCases.forEach((value) => {
it(`rejects "${value}"`, () => {
expect(isValid(value)).toBe(false);
});
});
});
});

View file

@ -8,7 +8,14 @@ import {
CROSS_ORIGIN_WARNING,
ISSUE_PROVIDER_COMMON_FORM_FIELDS,
} from '../../common-issue-form-stuff.const';
export const GITLAB_PROJECT_REGEX = /(^[1-9][0-9]*$)|((\/|%2F|\w-?|\.-?)+$)/i;
// Matches either a numeric project ID or a path (namespace/project-slug, or its
// %2F-encoded form). Anchored at both ends: without a leading `^` the original
// pattern only matched a valid *tail*, so display names with spaces (e.g.
// "My Group/My Gitlab") passed validation and produced a 404 at poll time (#8665).
// This is a "did you paste a display name?" sanity guard, not a strict GitLab
// path validator, so the char class stays permissive (e.g. accepts consecutive
// hyphens, which GitLab paths allow) rather than risk false-rejecting valid paths.
export const GITLAB_PROJECT_REGEX = /^(?:[1-9][0-9]*|[\w%./-]+)$/i;
export const GITLAB_CONFIG_FORM: LimitedFormlyFieldConfig<IssueProviderGitlab>[] = [
...CROSS_ORIGIN_WARNING,

View file

@ -422,7 +422,7 @@
"FILTER_USER": "Filter username",
"GITLAB_BASE_URL": "Custom GitLab base URL (optional)",
"PROJECT": "user name/project",
"PROJECT_HINT": "e.g. super-productivity/super-productivity",
"PROJECT_HINT": "Project path (namespace/project-slug) or numeric project ID, not the display name (e.g. super-productivity/super-productivity or 12345)",
"SCOPE": "Scope",
"SCOPE_ALL": "All",
"SCOPE_ASSIGNED": "Assigned to me",