Hardening of scope validation (#12490)

This commit is contained in:
Tom Moor
2026-05-27 18:27:34 -04:00
committed by GitHub
parent 1186ddd3c0
commit 0f2513346a
10 changed files with 288 additions and 27 deletions
+2 -1
View File
@@ -54,8 +54,9 @@ class ApiKey extends ParanoidModel<
name: string;
/** A list of scopes that this API key has access to */
@Matches(/[/.\w\s]*/, {
@Matches(AuthenticationHelper.scopeGrammarRegex, {
each: true,
message: "Scope must be a valid API scope",
})
@Column(DataType.ARRAY(DataType.STRING))
scope: string[] | null;
+2 -1
View File
@@ -83,8 +83,9 @@ class OAuthAuthentication extends ParanoidModel<
grantId: string | null;
/** A list of scopes that this authentication has access to */
@Matches(/[/.\w\s]*/, {
@Matches(AuthenticationHelper.scopeGrammarRegex, {
each: true,
message: "Scope must be a valid API scope",
})
@Column(DataType.ARRAY(DataType.STRING))
scope: string[];
@@ -8,6 +8,7 @@ import {
Table,
Length,
} from "sequelize-typescript";
import AuthenticationHelper from "@shared/helpers/AuthenticationHelper";
import { OAuthClientValidation } from "@shared/validations";
import env from "@server/env";
import User from "@server/models/User";
@@ -57,8 +58,9 @@ class OAuthAuthorizationCode extends IdModel<
grantId: string | null;
/** A list of scopes that this authorization code has access to */
@Matches(/[/.\w\s]*/, {
@Matches(AuthenticationHelper.scopeGrammarRegex, {
each: true,
message: "Scope must be a valid API scope",
})
@Column(DataType.ARRAY(DataType.STRING))
scope: string[];
@@ -353,5 +353,12 @@ describe("OAuthInterface", () => {
const result = await OAuthInterface.validateScope(user, client, scope);
expect(result).toBe(false);
});
it("should reject root wildcard route scopes", async () => {
const result = await OAuthInterface.validateScope(user, client, [
"/api/*.*",
]);
expect(result).toBe(false);
});
});
});
+5 -23
View File
@@ -5,7 +5,7 @@ import type {
User as OAuthUser,
} from "@node-oauth/oauth2-server";
import type { Required } from "utility-types";
import { Scope } from "@shared/types";
import AuthenticationHelper from "@shared/helpers/AuthenticationHelper";
import { isUrl } from "@shared/utils/urls";
import {
OAuthClient,
@@ -393,29 +393,11 @@ export const OAuthInterface: RefreshTokenModel &
}
const scopes = Array.isArray(scope) ? scope : [scope];
const validAccessScopes = Object.values(Scope);
return scopes.every((s: string) => {
if (validAccessScopes.includes(s as Scope)) {
return true;
}
const periodCount = (s.match(/\./g) || []).length;
const colonCount = (s.match(/:/g) || []).length;
if (periodCount === 1 && colonCount === 0) {
return true;
}
if (
colonCount === 1 &&
validAccessScopes.includes(s.split(":")[1] as Scope)
) {
return true;
}
return false;
})
// OAuth clients cannot request scopes that grant unrestricted access.
return scopes.every((s: string) =>
AuthenticationHelper.isValidScope(s, { allowRootWildcard: false })
)
? scopes
: false;
},