fix(security): authorize site uploads and harden tokens, media and request identity
This commit is contained in:
1 parent
52f6d1491f
commit
8abfe352ef
70 files changed
+1609
-204
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { parse } from "@babel/parser";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
it("keeps verification token minting and sending out of public server action exports", () => {
|
||||
const forbidden = [
|
||||
"verificationToken",
|
||||
"isValidVerificationToken",
|
||||
"sendVerification",
|
||||
];
|
||||
const exposed: string[] = [];
|
||||
for (const file of readdirSync("src/actions").filter(
|
||||
(file) => file.endsWith(".ts") && !file.endsWith(".test.ts"),
|
||||
)) {
|
||||
const ast = parse(readFileSync(path.join("src/actions", file), "utf8"), {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"],
|
||||
});
|
||||
if (!ast.program.directives.some((d) => d.value.value === "use server"))
|
||||
continue;
|
||||
for (const item of ast.program.body) {
|
||||
if (item.type !== "ExportNamedDeclaration") continue;
|
||||
if (
|
||||
item.declaration?.type === "FunctionDeclaration" &&
|
||||
forbidden.includes(item.declaration.id?.name ?? "")
|
||||
)
|
||||
exposed.push(`${file}:${item.declaration.id?.name}`);
|
||||
for (const spec of item.specifiers) {
|
||||
const name =
|
||||
spec.exported.type === "Identifier"
|
||||
? spec.exported.name
|
||||
: spec.exported.value;
|
||||
if (forbidden.includes(name)) exposed.push(`${file}:${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(exposed).toEqual([]);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
export type PersonalTokenAbility =
|
||||
| "tickets:read"
|
||||
| "tickets:write"
|
||||
| "articles:write"
|
||||
| "radio:read"
|
||||
| "radio:write"
|
||||
| "badges:read";
|
||||
|
||||
/** Sanctum abilities are JSON; invalid data never grants access. */
|
||||
export function tokenAllowsAbilities(
|
||||
encoded: unknown,
|
||||
required: readonly PersonalTokenAbility[] = [],
|
||||
): boolean {
|
||||
if (typeof encoded !== "string") return false;
|
||||
let abilities: unknown;
|
||||
try {
|
||||
abilities = JSON.parse(encoded);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!Array.isArray(abilities) ||
|
||||
abilities.length === 0 ||
|
||||
!abilities.every(
|
||||
(ability) =>
|
||||
typeof ability === "string" &&
|
||||
ability.length > 0 &&
|
||||
ability.trim() === ability,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (abilities.includes("*")) return true;
|
||||
// A caller with no declared scope requires a full-access token.
|
||||
return (
|
||||
required.length > 0 &&
|
||||
required.every((ability) => abilities.includes(ability))
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user