Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/harden-object-path-lookups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'astro': patch
'@astrojs/internal-helpers': patch
Comment thread
matthewp marked this conversation as resolved.
Outdated
'@astrojs/markdown-remark': patch
'create-astro': patch
---

Hardens nested object and package metadata lookups to ignore prototype keys in content handling and project scaffolding
7 changes: 7 additions & 0 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { CompiledCacheRoute } from './cache/runtime/route-matching.js';
import type { SessionDriverFactory } from './session/types.js';
import { NodePool } from '../runtime/server/render/queue/pool.js';
import { HTMLStringCache } from '../runtime/server/html-string-cache.js';
import { FORBIDDEN_PATH_KEYS } from '@astrojs/internal-helpers/object';

/**
* The `Pipeline` represents the static parts of rendering that do not change between requests.
Expand Down Expand Up @@ -287,6 +288,12 @@ export abstract class Pipeline {
}

for (const key of pathKeys) {
if (FORBIDDEN_PATH_KEYS.has(key)) {
throw new AstroError({
...ActionNotFoundError,
message: ActionNotFoundError.message(pathKeys.join('.')),
});
}
if (!Object.hasOwn(server, key)) {
throw new AstroError({
...ActionNotFoundError,
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/cache/memory-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ function parseVaryHeader(response: Response): string[] | undefined {
* Extract the values of Vary'd headers from a request.
*/
function getVaryValues(request: Request, varyHeaders: string[]): Record<string, string> {
const values: Record<string, string> = {};
const values = Object.create(null) as Record<string, string>;
Comment thread
ematipico marked this conversation as resolved.
for (const header of varyHeaders) {
values[header] = request.headers.get(header) ?? '';
}
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/src/preferences/dlv.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { FORBIDDEN_PATH_KEYS } from '@astrojs/internal-helpers/object';

export default function dlv(obj: Record<string, unknown>, key: string): any {
for (const k of key.split('.')) {
if (FORBIDDEN_PATH_KEYS.has(k) || !obj || typeof obj !== 'object' || !Object.hasOwn(obj, k)) {
return undefined;
}
// @ts-expect-error: Type 'unknown' is not assignable to type 'Record<string, unknown>'.
obj = obj?.[k];
obj = obj[k];
}
return obj;
}
13 changes: 4 additions & 9 deletions packages/create-astro/src/actions/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,10 @@ const FILES_TO_UPDATE = {
fs.promises.readFile(file, 'utf-8').then((value) => {
// Match first indent in the file or fall back to `\t`
const indent = /(^\s+)/m.exec(value)?.[1] ?? '\t';
return fs.promises.writeFile(
file,
JSON.stringify(
Object.assign(JSON.parse(value), Object.assign(overrides, { private: undefined })),
null,
indent,
),
'utf-8',
);
const packageJson = JSON.parse(value);
packageJson.name = overrides.name;
delete packageJson.private;
return fs.promises.writeFile(file, JSON.stringify(packageJson, null, indent), 'utf-8');
}),
};

Expand Down
6 changes: 5 additions & 1 deletion packages/internal-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"./fs": "./dist/fs.js",
"./cli": "./dist/cli.js",
"./create-filter": "./dist/create-filter.js",
"./request": "./dist/request.js"
"./request": "./dist/request.js",
"./object": "./dist/object.js"
},
"typesVersions": {
"*": {
Expand All @@ -35,6 +36,9 @@
],
"create-filter": [
"./dist/create-filter.d.ts"
],
"object": [
"./dist/object.d.ts"
]
}
},
Expand Down
5 changes: 5 additions & 0 deletions packages/internal-helpers/src/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Keys that must be rejected when traversing object paths (e.g. dot-separated
* property lookups) to prevent prototype-pollution attacks.
*/
export const FORBIDDEN_PATH_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
11 changes: 10 additions & 1 deletion packages/markdown/remark/src/rehype-collect-headings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { visit } from 'unist-util-visit';
import type { VFile } from 'vfile';
import type { MarkdownHeading, RehypePlugin } from './types.js';

import { FORBIDDEN_PATH_KEYS } from '@astrojs/internal-helpers/object';

const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
const codeTagNames = new Set(['code', 'pre']);

Expand Down Expand Up @@ -117,7 +119,14 @@ function getMdxFrontmatterVariableValue(frontmatter: Record<string, any>, path:
let value = frontmatter;

for (const key of path) {
if (!value[key]) return undefined;
if (
FORBIDDEN_PATH_KEYS.has(key) ||
!value ||
typeof value !== 'object' ||
!Object.hasOwn(value, key)
) {
return undefined;
}

value = value[key];
}
Expand Down
Loading