|
10 | 10 |
|
11 | 11 | 'use strict';
|
12 | 12 |
|
| 13 | +const heldValues = []; |
| 14 | +let finalizationCallback; |
| 15 | +function FinalizationRegistryMock(callback) { |
| 16 | + finalizationCallback = callback; |
| 17 | +} |
| 18 | +FinalizationRegistryMock.prototype.register = function (target, heldValue) { |
| 19 | + heldValues.push(heldValue); |
| 20 | +}; |
| 21 | +global.FinalizationRegistry = FinalizationRegistryMock; |
| 22 | + |
| 23 | +function gc() { |
| 24 | + for (let i = 0; i < heldValues.length; i++) { |
| 25 | + finalizationCallback(heldValues[i]); |
| 26 | + } |
| 27 | + heldValues.length = 0; |
| 28 | +} |
| 29 | + |
13 | 30 | let act;
|
14 | 31 | let use;
|
15 | 32 | let startTransition;
|
@@ -1446,4 +1463,202 @@ describe('ReactFlight', () => {
|
1446 | 1463 | );
|
1447 | 1464 | });
|
1448 | 1465 | });
|
| 1466 | + |
| 1467 | + // @gate enableTaint |
| 1468 | + it('errors when a tainted object is serialized', async () => { |
| 1469 | + function UserClient({user}) { |
| 1470 | + return <span>{user.name}</span>; |
| 1471 | + } |
| 1472 | + const User = clientReference(UserClient); |
| 1473 | + |
| 1474 | + const user = { |
| 1475 | + name: 'Seb', |
| 1476 | + age: 'rather not say', |
| 1477 | + }; |
| 1478 | + ReactServer.experimental_taintObjectReference( |
| 1479 | + "Don't pass the raw user object to the client", |
| 1480 | + user, |
| 1481 | + ); |
| 1482 | + const errors = []; |
| 1483 | + ReactNoopFlightServer.render(<User user={user} />, { |
| 1484 | + onError(x) { |
| 1485 | + errors.push(x.message); |
| 1486 | + }, |
| 1487 | + }); |
| 1488 | + |
| 1489 | + expect(errors).toEqual(["Don't pass the raw user object to the client"]); |
| 1490 | + }); |
| 1491 | + |
| 1492 | + // @gate enableTaint |
| 1493 | + it('errors with a specific message when a tainted function is serialized', async () => { |
| 1494 | + function UserClient({user}) { |
| 1495 | + return <span>{user.name}</span>; |
| 1496 | + } |
| 1497 | + const User = clientReference(UserClient); |
| 1498 | + |
| 1499 | + function change() {} |
| 1500 | + ReactServer.experimental_taintObjectReference( |
| 1501 | + 'A change handler cannot be passed to a client component', |
| 1502 | + change, |
| 1503 | + ); |
| 1504 | + const errors = []; |
| 1505 | + ReactNoopFlightServer.render(<User onChange={change} />, { |
| 1506 | + onError(x) { |
| 1507 | + errors.push(x.message); |
| 1508 | + }, |
| 1509 | + }); |
| 1510 | + |
| 1511 | + expect(errors).toEqual([ |
| 1512 | + 'A change handler cannot be passed to a client component', |
| 1513 | + ]); |
| 1514 | + }); |
| 1515 | + |
| 1516 | + // @gate enableTaint |
| 1517 | + it('errors when a tainted string is serialized', async () => { |
| 1518 | + function UserClient({user}) { |
| 1519 | + return <span>{user.name}</span>; |
| 1520 | + } |
| 1521 | + const User = clientReference(UserClient); |
| 1522 | + |
| 1523 | + const process = { |
| 1524 | + env: { |
| 1525 | + SECRET: '3e971ecc1485fe78625598bf9b6f85db', |
| 1526 | + }, |
| 1527 | + }; |
| 1528 | + ReactServer.experimental_taintUniqueValue( |
| 1529 | + 'Cannot pass a secret token to the client', |
| 1530 | + process, |
| 1531 | + process.env.SECRET, |
| 1532 | + ); |
| 1533 | + |
| 1534 | + const errors = []; |
| 1535 | + ReactNoopFlightServer.render(<User token={process.env.SECRET} />, { |
| 1536 | + onError(x) { |
| 1537 | + errors.push(x.message); |
| 1538 | + }, |
| 1539 | + }); |
| 1540 | + |
| 1541 | + expect(errors).toEqual(['Cannot pass a secret token to the client']); |
| 1542 | + |
| 1543 | + // This just ensures the process object is kept alive for the life time of |
| 1544 | + // the test since we're simulating a global as an example. |
| 1545 | + expect(process.env.SECRET).toBe('3e971ecc1485fe78625598bf9b6f85db'); |
| 1546 | + }); |
| 1547 | + |
| 1548 | + // @gate enableTaint |
| 1549 | + it('errors when a tainted bigint is serialized', async () => { |
| 1550 | + function UserClient({user}) { |
| 1551 | + return <span>{user.name}</span>; |
| 1552 | + } |
| 1553 | + const User = clientReference(UserClient); |
| 1554 | + |
| 1555 | + const currentUser = { |
| 1556 | + name: 'Seb', |
| 1557 | + token: BigInt('0x3e971ecc1485fe78625598bf9b6f85dc'), |
| 1558 | + }; |
| 1559 | + ReactServer.experimental_taintUniqueValue( |
| 1560 | + 'Cannot pass a secret token to the client', |
| 1561 | + currentUser, |
| 1562 | + currentUser.token, |
| 1563 | + ); |
| 1564 | + |
| 1565 | + function App({user}) { |
| 1566 | + return <User token={user.token} />; |
| 1567 | + } |
| 1568 | + |
| 1569 | + const errors = []; |
| 1570 | + ReactNoopFlightServer.render(<App user={currentUser} />, { |
| 1571 | + onError(x) { |
| 1572 | + errors.push(x.message); |
| 1573 | + }, |
| 1574 | + }); |
| 1575 | + |
| 1576 | + expect(errors).toEqual(['Cannot pass a secret token to the client']); |
| 1577 | + }); |
| 1578 | + |
| 1579 | + // @gate enableTaint && enableBinaryFlight |
| 1580 | + it('errors when a tainted binary value is serialized', async () => { |
| 1581 | + function UserClient({user}) { |
| 1582 | + return <span>{user.name}</span>; |
| 1583 | + } |
| 1584 | + const User = clientReference(UserClient); |
| 1585 | + |
| 1586 | + const currentUser = { |
| 1587 | + name: 'Seb', |
| 1588 | + token: new Uint32Array([0x3e971ecc, 0x1485fe78, 0x625598bf, 0x9b6f85dd]), |
| 1589 | + }; |
| 1590 | + ReactServer.experimental_taintUniqueValue( |
| 1591 | + 'Cannot pass a secret token to the client', |
| 1592 | + currentUser, |
| 1593 | + currentUser.token, |
| 1594 | + ); |
| 1595 | + |
| 1596 | + function App({user}) { |
| 1597 | + const clone = user.token.slice(); |
| 1598 | + return <User token={clone} />; |
| 1599 | + } |
| 1600 | + |
| 1601 | + const errors = []; |
| 1602 | + ReactNoopFlightServer.render(<App user={currentUser} />, { |
| 1603 | + onError(x) { |
| 1604 | + errors.push(x.message); |
| 1605 | + }, |
| 1606 | + }); |
| 1607 | + |
| 1608 | + expect(errors).toEqual(['Cannot pass a secret token to the client']); |
| 1609 | + }); |
| 1610 | + |
| 1611 | + // @gate enableTaint |
| 1612 | + it('keep a tainted value tainted until the end of any pending requests', async () => { |
| 1613 | + function UserClient({user}) { |
| 1614 | + return <span>{user.name}</span>; |
| 1615 | + } |
| 1616 | + const User = clientReference(UserClient); |
| 1617 | + |
| 1618 | + function getUser() { |
| 1619 | + const user = { |
| 1620 | + name: 'Seb', |
| 1621 | + token: '3e971ecc1485fe78625598bf9b6f85db', |
| 1622 | + }; |
| 1623 | + ReactServer.experimental_taintUniqueValue( |
| 1624 | + 'Cannot pass a secret token to the client', |
| 1625 | + user, |
| 1626 | + user.token, |
| 1627 | + ); |
| 1628 | + return user; |
| 1629 | + } |
| 1630 | + |
| 1631 | + function App() { |
| 1632 | + const user = getUser(); |
| 1633 | + const derivedValue = {...user}; |
| 1634 | + // A garbage collection can happen at any time. Even before the end of |
| 1635 | + // this request. This would clean up the user object. |
| 1636 | + gc(); |
| 1637 | + // We should still block the tainted value. |
| 1638 | + return <User user={derivedValue} />; |
| 1639 | + } |
| 1640 | + |
| 1641 | + let errors = []; |
| 1642 | + ReactNoopFlightServer.render(<App />, { |
| 1643 | + onError(x) { |
| 1644 | + errors.push(x.message); |
| 1645 | + }, |
| 1646 | + }); |
| 1647 | + |
| 1648 | + expect(errors).toEqual(['Cannot pass a secret token to the client']); |
| 1649 | + |
| 1650 | + // After the previous requests finishes, the token can be rendered again. |
| 1651 | + |
| 1652 | + errors = []; |
| 1653 | + ReactNoopFlightServer.render( |
| 1654 | + <User user={{token: '3e971ecc1485fe78625598bf9b6f85db'}} />, |
| 1655 | + { |
| 1656 | + onError(x) { |
| 1657 | + errors.push(x.message); |
| 1658 | + }, |
| 1659 | + }, |
| 1660 | + ); |
| 1661 | + |
| 1662 | + expect(errors).toEqual([]); |
| 1663 | + }); |
1449 | 1664 | });
|
0 commit comments