Compare commits

..

No commits in common. "d75c6065e70054513641c481844f50c1f986c395" and "0293655fc1292a0090ef96079444328f662786f3" have entirely different histories.

5 changed files with 8 additions and 36 deletions

View File

@ -22,23 +22,9 @@ import { useToast } from "~/hooks/use-toast";
import { useSubmit, useLoaderData } from "@remix-run/react"; import { useSubmit, useLoaderData } from "@remix-run/react";
import { User } from "~/utils/token-manager.server"; import { User } from "~/utils/token-manager.server";
// Only allow same-site, path-relative redirects. Rejects absolute URLs
// (https://evil.com) and protocol-relative URLs (//evil.com) to prevent
// open-redirect / phishing after login.
function safeRedirect(
to: FormDataEntryValue | string | null,
defaultRedirect = "/"
): string {
if (!to || typeof to !== "string") return defaultRedirect;
if (!to.startsWith("/") || to.startsWith("//") || to.startsWith("/\\")) {
return defaultRedirect;
}
return to;
}
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url); const url = new URL(request.url);
const redirectTo = safeRedirect(url.searchParams.get("redirectTo")); const redirectTo = url.searchParams.get("redirectTo") || "/";
return json({ redirectTo }); return json({ redirectTo });
} }
@ -304,7 +290,7 @@ export const meta: MetaFunction = () => {
export async function action({ request }: ActionFunctionArgs) { export async function action({ request }: ActionFunctionArgs) {
const clonedRequest = request.clone(); const clonedRequest = request.clone();
const formData = await clonedRequest.formData(); const formData = await clonedRequest.formData();
const redirectTo = safeRedirect(formData.get("redirectTo")); const redirectTo = (formData.get("redirectTo") as string) || "/";
try { try {
const user = (await authenticator.authenticate( const user = (await authenticator.authenticate(
@ -329,7 +315,7 @@ export async function action({ request }: ActionFunctionArgs) {
refresh_token_timestamp: currentTime, refresh_token_timestamp: currentTime,
}); });
return redirect(redirectTo, { return redirect(redirectTo || "/", {
headers: { headers: {
"Set-Cookie": await sessionStorage.commitSession(session, { "Set-Cookie": await sessionStorage.commitSession(session, {
expires: new Date(new Date().setMonth(new Date().getMonth() + 1)), expires: new Date(new Date().setMonth(new Date().getMonth() + 1)),

View File

@ -20,7 +20,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params; const { id } = params;
try { try {
const response = await fetch( const response = await fetch(
`${getInternalApiUrl()}/api/product/v1/products/${encodeURIComponent(id ?? "")}/` `${getInternalApiUrl()}/api/product/v1/products/${id}/`
); );
if (!response.ok) { if (!response.ok) {

View File

@ -59,7 +59,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
throw new Response("Seller ID is required", { status: 400 }); throw new Response("Seller ID is required", { status: 400 });
} }
try { try {
const response = await fetch(`${getInternalApiUrl()}/seller/${encodeURIComponent(sellerId)}/`); const response = await fetch(`${getInternalApiUrl()}/seller/${sellerId}/`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch seller: ${response.status}`); throw new Error(`Failed to fetch seller: ${response.status}`);

View File

@ -71,7 +71,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params; const { id } = params;
try { try {
const response = await fetch( const response = await fetch(
`${getInternalApiUrl()}/api/product/v1/products/${encodeURIComponent(id ?? "")}/`, `${getInternalApiUrl()}/api/product/v1/products/${id}/`,
); );
if (!response.ok) { if (!response.ok) {

View File

@ -1,17 +1,6 @@
import { createCookieSessionStorage, Session } from "@remix-run/node"; import { createCookieSessionStorage, Session } from "@remix-run/node";
import { isProduction } from "./utils/get-env"; import { isProduction } from "./utils/get-env";
const isProd = process.env.NODE_ENV === "production";
const sessionSecret = process.env.REMIX_SECRET;
// Fail closed: never ship a guessable signing secret to production. A signed
// cookie with a known secret can be forged, allowing session tampering.
if (isProd && !sessionSecret) {
throw new Error(
"REMIX_SECRET environment variable must be set in production."
);
}
// export the whole sessionStorage object // export the whole sessionStorage object
export const sessionStorage = createCookieSessionStorage({ export const sessionStorage = createCookieSessionStorage({
cookie: { cookie: {
@ -19,11 +8,8 @@ export const sessionStorage = createCookieSessionStorage({
sameSite: "lax", // this helps with CSRF sameSite: "lax", // this helps with CSRF
path: "/", // remember to add this so the cookie will work in all routes path: "/", // remember to add this so the cookie will work in all routes
httpOnly: true, // for security reasons, make this cookie http only httpOnly: true, // for security reasons, make this cookie http only
maxAge: 60 * 60 * 24 * 30, // 30 days (matches refresh-token lifetime) secrets: [process.env.REMIX_SECRET ?? "secret"], // replace this with an actual secret
// Dev-only fallback; production is guarded by the check above. secure: isProduction(), // enable this in prod only
secrets: [sessionSecret ?? "dev-only-insecure-secret"],
// Send only over HTTPS in production (NODE_ENV is the reliable server signal).
secure: isProd || isProduction(),
}, },
}); });
export const { getSession, commitSession, destroySession } = sessionStorage; export const { getSession, commitSession, destroySession } = sessionStorage;