Skip to content

Commit 843ec07

Browse files
authored
[Flight] Taint APIs (#27445)
This lets a registered object or value be "tainted", which we block from crossing the serialization boundary. It's only allowed to stay in-memory. This is an extra layer of protection against mistakes of transferring data from a data access layer to a client. It doesn't provide perfect protection, because it doesn't trace through derived values and substrings. So it shouldn't be used as the only security layer but more layers are better. `taintObjectReference` is for specific object instances, not any nested objects or values inside that object. It's useful to avoid specific objects from getting passed as is. It ensures that you don't accidentally leak values in a specific context. It can be for security reasons like tokens, privacy reasons like personal data or performance reasons like avoiding passing large objects over the wire. It might be privacy violation to leak the age of a specific user, but the number itself isn't blocked in any other context. As soon as the value is extracted and passed specifically without the object, it can therefore leak. `taintUniqueValue` is useful for high entropy values such as hashes, tokens or crypto keys that are very unique values. In that case it can be useful to taint the actual primitive values themselves. These can be encoded as a string, bigint or typed array. We don't currently check for this value in a substring or inside other typed arrays. Since values can be created from different sources they don't just follow garbage collection. In this case an additional object must be provided that defines the life time of this value for how long it should be blocked. It can be `globalThis` for essentially forever, but that risks leaking memory for ever when you're dealing with dynamic values like reading a token from a database. So in that case the idea is that you pass the object that might end up in cache. A request is the only thing that is expected to do any work. The principle is that you can derive values from out of a tainted entry during a request. Including stashing it in a per request cache. What you can't do is store a derived value in a global module level cache. At least not without also tainting the object.
1 parent 54baa79 commit 843ec07

23 files changed

+540
-5
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ module.exports = {
506506
Thenable: 'readonly',
507507
TimeoutID: 'readonly',
508508
WheelEventHandler: 'readonly',
509+
FinalizationRegistry: 'readonly',
509510

510511
spyOnDev: 'readonly',
511512
spyOnDevAndProd: 'readonly',

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@
1010

1111
'use strict';
1212

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+
1330
let act;
1431
let use;
1532
let startTransition;
@@ -1446,4 +1463,202 @@ describe('ReactFlight', () => {
14461463
);
14471464
});
14481465
});
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+
});
14491664
});

0 commit comments

Comments
 (0)