11// @ts -check
22
3- import { chmod , lstat , mkdir } from 'node:fs/promises' ;
3+ import { chmod , lstat , mkdir , open } from 'node:fs/promises' ;
44import { homedir } from 'node:os' ;
55import { dirname , join , resolve } from 'node:path' ;
66import { OWNERSHIP_MARKER_FILENAME , ensureOwnershipMarker } from './ownership.js' ;
@@ -36,7 +36,12 @@ export function resolveRuntimePaths(environment = process.env) {
3636}
3737
3838/**
39- * @param {{applicationDirectory: string, socketPath: string} } paths
39+ * @param {{
40+ * applicationDirectory: string,
41+ * socketPath: string,
42+ * supervisorStandardOutputPath?: string,
43+ * supervisorStandardErrorPath?: string
44+ * }} paths
4045 */
4146export async function prepareRuntimeDirectories ( paths ) {
4247 await ensurePrivateDirectory ( paths . applicationDirectory ) ;
@@ -48,6 +53,14 @@ export async function prepareRuntimeDirectories(paths) {
4853 if ( socketDirectory !== paths . applicationDirectory ) {
4954 await ensurePrivateDirectory ( socketDirectory ) ;
5055 }
56+ for ( const path of [
57+ paths . supervisorStandardOutputPath ,
58+ paths . supervisorStandardErrorPath ,
59+ ] ) {
60+ if ( path !== undefined ) {
61+ await ensurePrivateFile ( path ) ;
62+ }
63+ }
5164}
5265
5366/**
@@ -110,13 +123,65 @@ async function ensurePrivateDirectory(directory) {
110123 }
111124}
112125
126+ /**
127+ * Pre-create supervisor logs so native supervisors cannot create them with a
128+ * permissive inherited umask or follow an existing symbolic link.
129+ *
130+ * @param {string } path
131+ */
132+ async function ensurePrivateFile ( path ) {
133+ let information ;
134+ try {
135+ information = await lstat ( path ) ;
136+ } catch ( error ) {
137+ if ( ! isMissingFile ( error ) ) {
138+ throw error ;
139+ }
140+ let handle ;
141+ try {
142+ handle = await open ( path , 'wx' , 0o600 ) ;
143+ await handle . chmod ( 0o600 ) ;
144+ information = await handle . stat ( ) ;
145+ } catch ( creationError ) {
146+ if ( ! hasCode ( creationError , 'EEXIST' ) ) {
147+ throw creationError ;
148+ }
149+ information = await lstat ( path ) ;
150+ } finally {
151+ await handle ?. close ( ) ;
152+ }
153+ }
154+ if ( ! information . isFile ( ) || information . isSymbolicLink ( ) ) {
155+ throw new Error ( `Unsafe Portreeve runtime file: ${ path } ` ) ;
156+ }
157+ if ( typeof process . getuid === 'function' && information . uid !== process . getuid ( ) ) {
158+ throw new Error ( `Portreeve runtime file has another owner: ${ path } ` ) ;
159+ }
160+ if ( ( information . mode & 0o077 ) !== 0 ) {
161+ throw new Error (
162+ `Portreeve runtime file is not private: ${ path } (mode ${ ( information . mode & 0o777 ) . toString ( 8 ) } )` ,
163+ ) ;
164+ }
165+ if ( ( information . mode & 0o600 ) !== 0o600 ) {
166+ throw new Error ( `Portreeve runtime file lacks owner access: ${ path } ` ) ;
167+ }
168+ }
169+
113170/**
114171 * @param {unknown } error
115172 */
116173function isMissingFile ( error ) {
174+ return hasCode ( error , 'ENOENT' ) ;
175+ }
176+
177+ /**
178+ * @param {unknown } error
179+ * @param {string } code
180+ */
181+ function hasCode ( error , code ) {
117182 return (
118183 error instanceof Error &&
119184 'code' in error &&
120- /** @type {{code?: string} } */ ( error ) . code === 'ENOENT'
185+ /** @type {{code?: string} } */ ( error ) . code === code
121186 ) ;
122187}
0 commit comments