Skip to content

Commit

Permalink
Project import generated by Copybara.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 9f4cc2c6ca3f305636c5ea9f874db48d5afbcf76
  • Loading branch information
Redditbara authored and Kyle Maxwell committed Dec 13, 2024
1 parent 5782109 commit 1ac06fd
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 1 deletion.
4 changes: 3 additions & 1 deletion packages/public-api/src/devvit/Devvit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ export class Devvit extends Actor {
this.use(protos.HTTPDefinition);
}

if (pluginIsEnabled(config.kvStore) || pluginIsEnabled(config.redis)) {
// We're now defaulting this to on.
const redisNotSpecified = config.redis === undefined;
if (redisNotSpecified || pluginIsEnabled(config.kvStore) || pluginIsEnabled(config.redis)) {
this.use(protos.KVStoreDefinition);
this.use(protos.RedisAPIDefinition);
}
Expand Down
107 changes: 107 additions & 0 deletions packages/public-api/src/devvit/internals/csrf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { ContextActionRequest, HandleUIEventRequest } from '@devvit/protos';
import { Header } from '@devvit/shared-types/Header.js';

import type { BaseContext, ContextAPIClients } from '../../types/context.js';
import { getMenuItemById } from './menu-items.js';

type CSRF = {
needsModCheck: boolean;
};

export async function addCSRFTokenToContext(
context: BaseContext & ContextAPIClients,
req: ContextActionRequest
) {
const commentId = req.comment?.id && `t1_${req.comment.id}`;
const postId = req.post?.id && `t3_${req.post.id}`;
const subredditId = req.subreddit?.id && `t5_${req.subreddit.id}`;
const targetId = commentId || postId || subredditId;

if (!targetId) {
throw new Error('targetId is missing from ContextActionRequest');
}

const currentUser = context.debug.metadata[Header.User].values[0];
const subredditHeader = context.debug.metadata[Header.Subreddit].values[0];

if (!currentUser || !subredditHeader) {
throw new Error('User or subreddit missing from context');
}

if (commentId) {
const comment = await context.reddit.getCommentById(commentId);
if (!comment) {
throw new Error(`Comment ${commentId} not found`);
}
if (comment.subredditId !== subredditId) {
throw new Error('Comment does not belong to the subreddit');
}
}

if (postId) {
const post = await context.reddit.getPostById(postId);
if (!post) {
throw new Error(`Post ${postId} not found`);
}
if (post.subredditId !== subredditId) {
throw new Error('Post does not belong to the subreddit');
}
}

if (subredditId && subredditId !== subredditHeader) {
throw new Error(`Subreddit ${subredditId} ${subredditHeader} not found`);
}

const thisItem = getMenuItemById(req.actionId);
if (!thisItem) {
throw new Error('Action not found');
}
const needsModCheck = !!thisItem.forUserType?.includes('moderator');
if (needsModCheck && !isModerator(context)) {
throw new Error('User is not a moderator');
}

const val: CSRF = {
needsModCheck,
};

await context.redis.set(
`${currentUser}${subredditHeader}${targetId}${req.actionId}`,
JSON.stringify(val)
);
await context.redis.expire(`${currentUser}${subredditHeader}${targetId}${req.actionId}`, 600);
console.debug('CSRF token added: ' + JSON.stringify(val));
}

async function isModerator(context: BaseContext & ContextAPIClients) {
const currentUser = context.debug.metadata[Header.User].values[0];
if (!currentUser) {
throw new Error('User missing from context');
}
const subredditHeader = context.debug.metadata[Header.Subreddit].values[0];
if (!subredditHeader) {
throw new Error('Subreddit missing from context');
}

const subreddit = await context.reddit.getSubredditInfoById(subredditHeader);
const mods = await context.reddit.getModerators({ subredditName: subreddit.name! }).all();
return !!mods.find((mod) => mod.id === currentUser);
}

export async function validateCSRFToken(
context: BaseContext & ContextAPIClients,
req: HandleUIEventRequest
) {
const currentUser = context.debug.metadata[Header.User].values[0];
const subredditHeader = context.debug.metadata[Header.Subreddit].values[0];
const { actionId, thingId } = req.state?.__contextAction ?? {};
const csrfData = await context.redis.get(`${currentUser}${subredditHeader}${thingId}${actionId}`);
if (!csrfData) {
throw new Error('CSRF token not found');
}
const csrf = JSON.parse(csrfData) as CSRF;
if (csrf.needsModCheck && !(await isModerator(context))) {
throw new Error('User is not a moderator: ' + currentUser + '; ' + subredditHeader);
}
console.debug('CSRF token validated: ' + csrfData);
}
3 changes: 3 additions & 0 deletions packages/public-api/src/devvit/internals/menu-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getEffectsFromUIClient } from '../../apis/ui/helpers/getEffectsFromUICl
import type { MenuItem, MenuItemOnPressEvent } from '../../types/index.js';
import { Devvit } from '../Devvit.js';
import { getContextFromMetadata } from './context.js';
import { addCSRFTokenToContext } from './csrf.js';
import { extendDevvitPrototype } from './helpers/extendDevvitPrototype.js';

const getActionId = (index: number): string => {
Expand Down Expand Up @@ -87,6 +88,8 @@ async function onAction(
}
);

await addCSRFTokenToContext(context, req);

await menuItem.onPress(event, context);

return ContextActionResponse.fromJSON({
Expand Down
3 changes: 3 additions & 0 deletions packages/public-api/src/devvit/internals/ui-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getFormValues } from '../../apis/ui/helpers/getFormValues.js';
import { Devvit } from '../Devvit.js';
import { BlocksReconciler } from './blocks/BlocksReconciler.js';
import { getContextFromMetadata } from './context.js';
import { validateCSRFToken } from './csrf.js';
import { extendDevvitPrototype } from './helpers/extendDevvitPrototype.js';
import { getMenuItemById } from './menu-items.js';

Expand Down Expand Up @@ -82,6 +83,8 @@ async function handleUIEvent(
}
);

await validateCSRFToken(context, req);

await formDefinition.onSubmit(
{
values: getFormValues(req.event.formSubmitted.results),
Expand Down

0 comments on commit 1ac06fd

Please sign in to comment.