@@ -163,6 +163,48 @@ describe('test/tar/security-GHSA-grx6-75c5-f738.test.js', () => { |
163 | 163 | assert.strictEqual(fs.realpathSync(path.join(destDir, 'new/self')), fs.realpathSync(path.join(destDir, 'new'))); |
164 | 164 | }); |
165 | 165 |
|
| 166 | + it('should extract a file through an internal directory symlink', async () => { |
| 167 | + const destDir = path.join(tempDir, 'dest'); |
| 168 | + await extract([ |
| 169 | + { name: 'target/', type: 'directory' }, |
| 170 | + { name: 'link', type: 'symlink', linkname: 'target' }, |
| 171 | + { name: 'link/file.txt', content: 'safe' }, |
| 172 | + ], destDir); |
| 173 | + |
| 174 | + const link = path.join(destDir, 'link'); |
| 175 | + assert.strictEqual(fs.lstatSync(link).isSymbolicLink(), true); |
| 176 | + assert.strictEqual(fs.statSync(link).isDirectory(), true); |
| 177 | + assert.strictEqual(fs.readlinkSync(link), 'target'); |
| 178 | + assert.strictEqual(fs.readFileSync(path.join(destDir, 'target/file.txt'), 'utf8'), 'safe'); |
| 179 | + }); |
| 180 | + |
| 181 | + [ 'file', 'directory' ].forEach(type => { |
| 182 | + it(`should infer a ${type} target from the real parent of a symlink`, async () => { |
| 183 | + const destDir = path.join(tempDir, 'dest'); |
| 184 | + const isDirectory = type === 'directory'; |
| 185 | + await extract([ |
| 186 | + { name: 'a/b/', type: 'directory' }, |
| 187 | + { name: 'a/target', type, content: 'safe' }, |
| 188 | + // The textual parent resolves ../target to this entry of the opposite type. |
| 189 | + { name: 'target', type: isDirectory ? 'file' : 'directory', content: 'decoy' }, |
| 190 | + { name: 'd', type: 'symlink', linkname: 'a/b' }, |
| 191 | + { name: 'd/link', type: 'symlink', linkname: '../target' }, |
| 192 | + ], destDir); |
| 193 | + |
| 194 | + const link = path.join(destDir, 'd/link'); |
| 195 | + assert.strictEqual(fs.lstatSync(link).isSymbolicLink(), true); |
| 196 | + assert.strictEqual(fs.statSync(link).isDirectory(), isDirectory); |
| 197 | + assert.strictEqual(fs.readlinkSync(link), path.join('..', 'target')); |
| 198 | + assert.strictEqual(fs.realpathSync(link), fs.realpathSync(path.join(destDir, 'a/target'))); |
| 199 | + if (isDirectory) { |
| 200 | + fs.writeFileSync(path.join(link, 'file.txt'), 'safe'); |
| 201 | + assert.strictEqual(fs.readFileSync(path.join(destDir, 'a/target/file.txt'), 'utf8'), 'safe'); |
| 202 | + } else { |
| 203 | + assert.strictEqual(fs.readFileSync(link, 'utf8'), 'safe'); |
| 204 | + } |
| 205 | + }); |
| 206 | + }); |
| 207 | + |
166 | 208 | it('should block an internal target that resolves through an external symlink', async () => { |
167 | 209 | const destDir = path.join(tempDir, 'dest'); |
168 | 210 | const outsideDir = path.join(tempDir, 'outside'); |
@@ -204,7 +246,9 @@ describe('test/tar/security-GHSA-grx6-75c5-f738.test.js', () => { |
204 | 246 | fs.mkdirSync(realBase); |
205 | 247 | fs.symlinkSync(realBase, alias, 'dir'); |
206 | 248 | const destDir = path.join(alias, 'dest'); |
207 | | - const realDestDir = path.join(fs.realpathSync(realBase), 'dest'); |
| 249 | + // Match the extractor's native realpath resolution, including Windows |
| 250 | + // short paths such as RUNNER~1. |
| 251 | + const realDestDir = path.join(await fs.promises.realpath(realBase), 'dest'); |
208 | 252 |
|
209 | 253 | const entries = [ |
210 | 254 | { name: 'nested/relative', type: 'symlink', linkname: '../target.txt' }, |
|