mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Hardening of scope validation (#12490)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user