@@ -57,6 +57,8 @@ import {
5757 preparePersistentScanRoot ,
5858 requirePrivateCredentialHome ,
5959 requirePrivateOutputDirectory ,
60+ requireSecureOutputAncestry ,
61+ requireTrustedOutputAncestor ,
6062 runWorkbench ,
6163 setCodexSecurityCredentialLogout ,
6264} from "../src/runtime.js" ;
@@ -1664,6 +1666,80 @@ describe("runtime directories and plugin Python boundary", () => {
16641666 ) . not . toThrow ( ) ;
16651667 } ) ;
16661668
1669+ testPosix (
1670+ "rejects scan output under a non-sticky shared parent directory" ,
1671+ async ( ) => {
1672+ const root = await temporaryDirectory ( ) ;
1673+ const shared = join ( root , "shared" ) ;
1674+ await mkdir ( shared , { mode : 0o777 } ) ;
1675+ await chmod ( shared , 0o777 ) ;
1676+ const output = join ( shared , "results" ) ;
1677+
1678+ await expect ( prepareOutputDir ( output , "repo" ) ) . rejects . toThrow (
1679+ "sticky bit" ,
1680+ ) ;
1681+ await expect ( requireSecureOutputAncestry ( output ) ) . rejects . toThrow (
1682+ "sticky bit" ,
1683+ ) ;
1684+ } ,
1685+ ) ;
1686+
1687+ testPosix (
1688+ "accepts scan output under a sticky shared parent directory" ,
1689+ async ( ) => {
1690+ const root = await temporaryDirectory ( ) ;
1691+ const shared = join ( root , "shared" ) ;
1692+ await mkdir ( shared , { mode : 0o1777 } ) ;
1693+ await chmod ( shared , 0o1777 ) ;
1694+ // Some filesystems (notably user dirs on macOS APFS) ignore sticky on
1695+ // chmod; fall back to the process temp root when it is already sticky.
1696+ let stickyParent = shared ;
1697+ if ( ( ( await lstat ( shared ) ) . mode & 0o1000 ) === 0 ) {
1698+ stickyParent = await realpath ( tmpdir ( ) ) ;
1699+ if ( ( ( await lstat ( stickyParent ) ) . mode & 0o1000 ) === 0 ) {
1700+ return ;
1701+ }
1702+ }
1703+ const output = join (
1704+ stickyParent ,
1705+ `codex-security-sticky-${ process . pid } -${ Date . now ( ) } ` ,
1706+ ) ;
1707+ temporaryDirectories . push ( output ) ;
1708+
1709+ await expect (
1710+ requireSecureOutputAncestry ( output ) ,
1711+ ) . resolves . toBeUndefined ( ) ;
1712+ expect ( await prepareOutputDir ( output , "repo" ) ) . toBe ( output ) ;
1713+ } ,
1714+ ) ;
1715+
1716+ testPosix ( "rejects sticky shared parents controlled by another user" , ( ) => {
1717+ expect ( ( ) =>
1718+ requireTrustedOutputAncestor (
1719+ { mode : 0o41777 , uid : 1001 } ,
1720+ "/shared" ,
1721+ 1000 ,
1722+ ) ,
1723+ ) . toThrow ( "trusted owner" ) ;
1724+ expect ( ( ) =>
1725+ requireTrustedOutputAncestor (
1726+ { mode : 0o40755 , uid : 1001 } ,
1727+ "/other-user" ,
1728+ 1000 ,
1729+ ) ,
1730+ ) . toThrow ( "trusted owner" ) ;
1731+ expect ( ( ) =>
1732+ requireTrustedOutputAncestor (
1733+ { mode : 0o41777 , uid : 1000 } ,
1734+ "/shared" ,
1735+ 1000 ,
1736+ ) ,
1737+ ) . not . toThrow ( ) ;
1738+ expect ( ( ) =>
1739+ requireTrustedOutputAncestor ( { mode : 0o41777 , uid : 0 } , "/tmp" , 1000 ) ,
1740+ ) . not . toThrow ( ) ;
1741+ } ) ;
1742+
16671743 test ( "archives a non-empty private output directory" , async ( ) => {
16681744 const root = await temporaryDirectory ( ) ;
16691745 const output = join ( root , "scan" ) ;
0 commit comments