feat: sealing robustness (#1170)

A series of changes to improve sealing robustness avoiding errors that
have appeared during monitoring.

Additionally handles the recently published CVE affecting the
`pdfjs-dist` library.
This commit is contained in:
Lucas Smith
2024-05-23 15:35:05 +10:00
committed by GitHub
16 changed files with 211 additions and 56655 deletions

View File

@@ -18,6 +18,10 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
);
const FONT_NOTO_SANS_BYTES = fs.readFileSync(
path.join(__dirname, '../../packages/assets/fonts/noto-sans.ttf'),
);
/** @type {import('next').NextConfig} */
const config = {
experimental: {
@@ -38,6 +42,7 @@ const config = {
env: {
NEXT_PUBLIC_PROJECT: 'marketing',
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
},
modularizeImports: {
'lucide-react': {

File diff suppressed because one or more lines are too long

View File

@@ -18,6 +18,10 @@ const FONT_CAVEAT_BYTES = fs.readFileSync(
path.join(__dirname, '../../packages/assets/fonts/caveat.ttf'),
);
const FONT_NOTO_SANS_BYTES = fs.readFileSync(
path.join(__dirname, '../../packages/assets/fonts/noto-sans.ttf'),
);
/** @type {import('next').NextConfig} */
const config = {
output: process.env.DOCKER_OUTPUT ? 'standalone' : undefined,
@@ -42,6 +46,7 @@ const config = {
APP_VERSION: version,
NEXT_PUBLIC_PROJECT: 'web',
FONT_CAVEAT_URI: `data:font/ttf;base64,${FONT_CAVEAT_BYTES.toString('base64')}`,
FONT_NOTO_SANS_URI: `data:font/ttf;base64,${FONT_NOTO_SANS_BYTES.toString('base64')}`,
},
modularizeImports: {
'lucide-react': {

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,8 @@
import Link from 'next/link';
import { type Document, DocumentStatus } from '@documenso/prisma/client';
import type { Recipient } from '@documenso/prisma/client';
import { type Document, SigningStatus } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -17,9 +18,10 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type AdminActionsProps = {
className?: string;
document: Document;
recipients: Recipient[];
};
export const AdminActions = ({ className, document }: AdminActionsProps) => {
export const AdminActions = ({ className, document, recipients }: AdminActionsProps) => {
const { toast } = useToast();
const { mutate: resealDocument, isLoading: isResealDocumentLoading } =
@@ -47,7 +49,9 @@ export const AdminActions = ({ className, document }: AdminActionsProps) => {
<Button
variant="outline"
loading={isResealDocumentLoading}
disabled={document.status !== DocumentStatus.COMPLETED}
disabled={recipients.some(
(recipient) => recipient.signingStatus !== SigningStatus.SIGNED,
)}
onClick={() => resealDocument({ id: document.id })}
>
Reseal document

View File

@@ -53,7 +53,7 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
<h2 className="text-lg font-semibold">Admin Actions</h2>
<AdminActions className="mt-2" document={document} />
<AdminActions className="mt-2" document={document} recipients={document.Recipient} />
<hr className="my-4" />
<h2 className="text-lg font-semibold">Recipients</h2>

108
package-lock.json generated
View File

@@ -17536,6 +17536,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz",
"integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -17580,18 +17581,15 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/pdfjs-dist": {
"version": "3.6.172",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.6.172.tgz",
"integrity": "sha512-bfOhCg+S9DXh/ImWhWYTOiq3aVMFSCvzGiBzsIJtdMC71kVWDBw7UXr32xh0y56qc5wMVylIeqV3hBaRsu+e+w==",
"dependencies": {
"path2d-polyfill": "^2.0.1",
"web-streams-polyfill": "^3.2.1"
},
"version": "3.11.174",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
"integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==",
"engines": {
"node": ">=16"
"node": ">=18"
},
"optionalDependencies": {
"canvas": "^2.11.2"
"canvas": "^2.11.2",
"path2d-polyfill": "^2.0.1"
}
},
"node_modules/peberminta": {
@@ -19011,42 +19009,6 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-pdf": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz",
"integrity": "sha512-d7WAxcsjOogJfJ+I+zX/mdip3VjR1yq/yDa4hax4XbQVjbbbup6rqs4c8MGx0MLSnzob17TKp1t4CsNbDZ6GeQ==",
"dependencies": {
"clsx": "^2.0.0",
"make-cancellable-promise": "^1.3.1",
"make-event-props": "^1.6.0",
"merge-refs": "^1.2.1",
"pdfjs-dist": "3.6.172",
"prop-types": "^15.6.2",
"tiny-invariant": "^1.0.0",
"tiny-warning": "^1.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-pdf/node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/react-property": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz",
@@ -21357,11 +21319,6 @@
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/tinybench": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz",
@@ -22986,6 +22943,14 @@
"node": ">=12.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@@ -25390,11 +25355,13 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.2",
"next": "14.0.3",
"pdfjs-dist": "3.6.172",
"pdfjs-dist": "3.11.174",
"react": "18.2.0",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.45.4",
"react-pdf": "7.3.3",
"react-pdf": "7.7.3",
"react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5",
@@ -25411,6 +25378,43 @@
"typescript": "5.2.2"
}
},
"packages/ui/node_modules/react-pdf": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.3.tgz",
"integrity": "sha512-a2VfDl8hiGjugpqezBTUzJHYLNB7IS7a2t7GD52xMI9xHg8LdVaTMsnM9ZlNmKadnStT/tvX5IfV0yLn+JvYmw==",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"make-cancellable-promise": "^1.3.1",
"make-event-props": "^1.6.0",
"merge-refs": "^1.2.1",
"pdfjs-dist": "3.11.174",
"prop-types": "^15.6.2",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"packages/ui/node_modules/react-pdf/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"packages/ui/node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import { APP_BASE_URL } from './app';
export const DEFAULT_STANDARD_FONT_SIZE = 15;
export const DEFAULT_STANDARD_FONT_SIZE = 12;
export const DEFAULT_HANDWRITING_FONT_SIZE = 50;
export const MIN_STANDARD_FONT_SIZE = 8;

View File

@@ -17,6 +17,7 @@ import { getFile } from '../../universal/upload/get-file';
import { putPdfFile } from '../../universal/upload/put-file';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations';
import { flattenForm } from '../pdf/flatten-form';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
@@ -101,7 +102,7 @@ export const sealDocument = async ({
// Normalize and flatten layers that could cause issues with the signature
normalizeSignatureAppearances(doc);
doc.getForm().flatten();
flattenForm(doc);
flattenAnnotations(doc);
if (certificate) {

View File

@@ -0,0 +1,112 @@
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
import { PDFCheckBox, PDFRadioGroup, PDFRef } from 'pdf-lib';
import {
PDFDict,
type PDFDocument,
PDFName,
drawObject,
popGraphicsState,
pushGraphicsState,
rotateInPlace,
translate,
} from 'pdf-lib';
export const flattenForm = (document: PDFDocument) => {
const form = document.getForm();
form.updateFieldAppearances();
for (const field of form.getFields()) {
for (const widget of field.acroField.getWidgets()) {
flattenWidget(document, field, widget);
}
try {
form.removeField(field);
} catch (error) {
console.error(error);
}
}
};
const getPageForWidget = (document: PDFDocument, widget: PDFWidgetAnnotation) => {
const pageRef = widget.P();
let page = document.getPages().find((page) => page.ref === pageRef);
if (!page) {
const widgetRef = document.context.getObjectRef(widget.dict);
if (!widgetRef) {
return null;
}
page = document.findPageForAnnotationRef(widgetRef);
if (!page) {
return null;
}
}
return page;
};
const getAppearanceRefForWidget = (field: PDFField, widget: PDFWidgetAnnotation) => {
try {
const normalAppearance = widget.getNormalAppearance();
let normalAppearanceRef: PDFRef | null = null;
if (normalAppearance instanceof PDFRef) {
normalAppearanceRef = normalAppearance;
}
if (
normalAppearance instanceof PDFDict &&
(field instanceof PDFCheckBox || field instanceof PDFRadioGroup)
) {
const value = field.acroField.getValue();
const ref = normalAppearance.get(value) ?? normalAppearance.get(PDFName.of('Off'));
if (ref instanceof PDFRef) {
normalAppearanceRef = ref;
}
}
return normalAppearanceRef;
} catch (error) {
console.error(error);
return null;
}
};
const flattenWidget = (document: PDFDocument, field: PDFField, widget: PDFWidgetAnnotation) => {
try {
const page = getPageForWidget(document, widget);
if (!page) {
return;
}
const appearanceRef = getAppearanceRefForWidget(field, widget);
if (!appearanceRef) {
return;
}
const xObjectKey = page.node.newXObject('FlatWidget', appearanceRef);
const rectangle = widget.getRectangle();
const operators = [
pushGraphicsState(),
translate(rectangle.x, rectangle.y),
...rotateInPlace({ ...rectangle, rotation: 0 }),
drawObject(xObjectKey),
popGraphicsState(),
].filter((op) => !!op);
page.pushOperators(...operators);
} catch (error) {
console.error(error);
}
};

View File

@@ -1,6 +1,6 @@
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument, StandardFonts } from 'pdf-lib';
import { PDFDocument } from 'pdf-lib';
import {
DEFAULT_HANDWRITING_FONT_SIZE,
@@ -17,6 +17,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
res.arrayBuffer(),
);
const fontNoto = await fetch(process.env.FONT_NOTO_SANS_URI).then(async (res) =>
res.arrayBuffer(),
);
const isSignatureField = isSignatureFieldType(field.type);
pdf.registerFontkit(fontkit);
@@ -41,7 +45,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const fieldX = pageWidth * (Number(field.positionX) / 100);
const fieldY = pageHeight * (Number(field.positionY) / 100);
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
const font = await pdf.embedFont(isSignatureField ? fontCaveat : fontNoto);
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await pdf.embedFont(fontCaveat);

View File

@@ -1,6 +1,7 @@
import { TRPCError } from '@trpc/server';
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
@@ -10,6 +11,7 @@ import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upse
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { DocumentStatus } from '@documenso/prisma/client';
import { adminProcedure, router } from '../trpc';
import {
@@ -100,7 +102,11 @@ export const adminRouter = router({
const { id } = input;
try {
return await sealDocument({ documentId: id, isResealing: true });
const document = await getEntireDocument({ id });
const isResealing = document.status === DocumentStatus.COMPLETED;
return await sealDocument({ documentId: id, isResealing });
} catch (err) {
console.error('resealDocument error', err);

View File

@@ -73,6 +73,7 @@ declare namespace NodeJS {
DEPLOYMENT_TARGET?: 'webapp' | 'marketing';
FONT_CAVEAT_URI: string;
FONT_NOTO_SANS_URI: string;
POSTGRES_URL?: string;
DATABASE_URL?: string;

View File

@@ -63,15 +63,17 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.2",
"next": "14.0.3",
"pdfjs-dist": "3.6.172",
"pdfjs-dist": "3.11.174",
"react": "18.2.0",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.45.4",
"react-pdf": "7.3.3",
"react-pdf": "7.7.3",
"react-rnd": "^10.4.1",
"tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5",
"zod": "^3.22.4"
}
}
}

View File

@@ -112,6 +112,7 @@
"NODE_ENV",
"DEPLOYMENT_TARGET",
"FONT_CAVEAT_URI",
"FONT_NOTO_SANS_URI",
"POSTGRES_URL",
"DATABASE_URL",
"DATABASE_URL_UNPOOLED",