nextjs-secure 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +656 -501
- package/dist/audit.cjs +1337 -0
- package/dist/audit.cjs.map +1 -0
- package/dist/audit.d.cts +679 -0
- package/dist/audit.d.ts +679 -0
- package/dist/audit.js +1300 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth.cjs +500 -6
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +180 -19
- package/dist/auth.d.ts +180 -19
- package/dist/auth.js +493 -6
- package/dist/auth.js.map +1 -1
- package/dist/index.cjs +3743 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3668 -8
- package/dist/index.js.map +1 -1
- package/dist/path-BVbunPfR.d.cts +534 -0
- package/dist/path-BVbunPfR.d.ts +534 -0
- package/dist/validation.cjs +2031 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +42 -0
- package/dist/validation.d.ts +42 -0
- package/dist/validation.js +1964 -0
- package/dist/validation.js.map +1 -0
- package/package.json +26 -1
package/dist/index.cjs
CHANGED
|
@@ -869,8 +869,8 @@ function withRateLimit(handler, config) {
|
|
|
869
869
|
};
|
|
870
870
|
try {
|
|
871
871
|
if (finalConfig.skip) {
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
872
|
+
const shouldSkip2 = await finalConfig.skip(request);
|
|
873
|
+
if (shouldSkip2) {
|
|
874
874
|
debug("Skipping rate limit check");
|
|
875
875
|
return handler(request, ctx);
|
|
876
876
|
}
|
|
@@ -946,8 +946,8 @@ async function checkRateLimit(request, config) {
|
|
|
946
946
|
const windowMs = parseDuration(finalConfig.window);
|
|
947
947
|
const algorithm = getAlgorithm(finalConfig.algorithm);
|
|
948
948
|
if (finalConfig.skip) {
|
|
949
|
-
const
|
|
950
|
-
if (
|
|
949
|
+
const shouldSkip2 = await finalConfig.skip(request);
|
|
950
|
+
if (shouldSkip2) {
|
|
951
951
|
const info2 = {
|
|
952
952
|
limit: finalConfig.limit,
|
|
953
953
|
remaining: finalConfig.limit,
|
|
@@ -1123,8 +1123,8 @@ function withCSRF(handler, config = {}) {
|
|
|
1123
1123
|
return handler(req);
|
|
1124
1124
|
}
|
|
1125
1125
|
if (config.skip) {
|
|
1126
|
-
const
|
|
1127
|
-
if (
|
|
1126
|
+
const shouldSkip2 = await config.skip(req);
|
|
1127
|
+
if (shouldSkip2) return handler(req);
|
|
1128
1128
|
}
|
|
1129
1129
|
const cookieName = cookieOpts.name || "__csrf";
|
|
1130
1130
|
const cookieToken = req.cookies.get(cookieName)?.value;
|
|
@@ -1446,20 +1446,3692 @@ function createSecurityHeadersObject(options = {}) {
|
|
|
1446
1446
|
});
|
|
1447
1447
|
return obj;
|
|
1448
1448
|
}
|
|
1449
|
+
var encoder2 = new TextEncoder();
|
|
1450
|
+
var decoder = new TextDecoder();
|
|
1451
|
+
function base64UrlDecode(str) {
|
|
1452
|
+
const pad = str.length % 4;
|
|
1453
|
+
if (pad) {
|
|
1454
|
+
str += "=".repeat(4 - pad);
|
|
1455
|
+
}
|
|
1456
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1457
|
+
const binary = atob(base64);
|
|
1458
|
+
const bytes = new Uint8Array(binary.length);
|
|
1459
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1460
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1461
|
+
}
|
|
1462
|
+
return bytes;
|
|
1463
|
+
}
|
|
1464
|
+
function decodeJWT(token) {
|
|
1465
|
+
try {
|
|
1466
|
+
const parts = token.split(".");
|
|
1467
|
+
if (parts.length !== 3) return null;
|
|
1468
|
+
const header = JSON.parse(decoder.decode(base64UrlDecode(parts[0])));
|
|
1469
|
+
const payload = JSON.parse(decoder.decode(base64UrlDecode(parts[1])));
|
|
1470
|
+
const signature = base64UrlDecode(parts[2]);
|
|
1471
|
+
return { header, payload, signature };
|
|
1472
|
+
} catch {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
function getAlgorithmParams(alg) {
|
|
1477
|
+
switch (alg) {
|
|
1478
|
+
case "HS256":
|
|
1479
|
+
return { name: "HMAC", hash: "SHA-256" };
|
|
1480
|
+
case "HS384":
|
|
1481
|
+
return { name: "HMAC", hash: "SHA-384" };
|
|
1482
|
+
case "HS512":
|
|
1483
|
+
return { name: "HMAC", hash: "SHA-512" };
|
|
1484
|
+
case "RS256":
|
|
1485
|
+
return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
1486
|
+
case "RS384":
|
|
1487
|
+
return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" };
|
|
1488
|
+
case "RS512":
|
|
1489
|
+
return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" };
|
|
1490
|
+
case "ES256":
|
|
1491
|
+
return { name: "ECDSA", hash: "SHA-256", namedCurve: "P-256" };
|
|
1492
|
+
case "ES384":
|
|
1493
|
+
return { name: "ECDSA", hash: "SHA-384", namedCurve: "P-384" };
|
|
1494
|
+
case "ES512":
|
|
1495
|
+
return { name: "ECDSA", hash: "SHA-512", namedCurve: "P-521" };
|
|
1496
|
+
default:
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async function verifyHMAC(data, signature, secret, hash2) {
|
|
1501
|
+
const key = await crypto$1.webcrypto.subtle.importKey(
|
|
1502
|
+
"raw",
|
|
1503
|
+
encoder2.encode(secret),
|
|
1504
|
+
{ name: "HMAC", hash: hash2 },
|
|
1505
|
+
false,
|
|
1506
|
+
["verify"]
|
|
1507
|
+
);
|
|
1508
|
+
return crypto$1.webcrypto.subtle.verify("HMAC", key, signature, encoder2.encode(data));
|
|
1509
|
+
}
|
|
1510
|
+
async function importPublicKey(pem, algorithm) {
|
|
1511
|
+
const pemContents = pem.replace(/-----BEGIN.*-----/, "").replace(/-----END.*-----/, "").replace(/\s/g, "");
|
|
1512
|
+
const binaryDer = base64UrlDecode(pemContents.replace(/\+/g, "-").replace(/\//g, "_"));
|
|
1513
|
+
const keyUsages = ["verify"];
|
|
1514
|
+
if (algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
1515
|
+
return crypto$1.webcrypto.subtle.importKey(
|
|
1516
|
+
"spki",
|
|
1517
|
+
binaryDer,
|
|
1518
|
+
{ name: algorithm.name, hash: algorithm.hash },
|
|
1519
|
+
false,
|
|
1520
|
+
keyUsages
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
if (algorithm.name === "ECDSA") {
|
|
1524
|
+
return crypto$1.webcrypto.subtle.importKey(
|
|
1525
|
+
"spki",
|
|
1526
|
+
binaryDer,
|
|
1527
|
+
{ name: algorithm.name, namedCurve: algorithm.namedCurve },
|
|
1528
|
+
false,
|
|
1529
|
+
keyUsages
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
throw new Error(`Unsupported algorithm: ${algorithm.name}`);
|
|
1533
|
+
}
|
|
1534
|
+
async function verifyAsymmetric(data, signature, publicKey, algorithm) {
|
|
1535
|
+
const key = await importPublicKey(publicKey, algorithm);
|
|
1536
|
+
const params = algorithm.name === "ECDSA" ? { name: "ECDSA", hash: algorithm.hash } : algorithm.name;
|
|
1537
|
+
return crypto$1.webcrypto.subtle.verify(params, key, signature, encoder2.encode(data));
|
|
1538
|
+
}
|
|
1539
|
+
function validateClaims(payload, config) {
|
|
1540
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1541
|
+
const tolerance = config.clockTolerance || 0;
|
|
1542
|
+
if (payload.exp !== void 0 && payload.exp < now - tolerance) {
|
|
1543
|
+
return {
|
|
1544
|
+
code: "expired_token",
|
|
1545
|
+
message: "Token has expired",
|
|
1546
|
+
status: 401
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
if (payload.nbf !== void 0 && payload.nbf > now + tolerance) {
|
|
1550
|
+
return {
|
|
1551
|
+
code: "invalid_token",
|
|
1552
|
+
message: "Token not yet valid",
|
|
1553
|
+
status: 401
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
if (config.issuer) {
|
|
1557
|
+
const issuers = Array.isArray(config.issuer) ? config.issuer : [config.issuer];
|
|
1558
|
+
if (!payload.iss || !issuers.includes(payload.iss)) {
|
|
1559
|
+
return {
|
|
1560
|
+
code: "invalid_token",
|
|
1561
|
+
message: "Invalid token issuer",
|
|
1562
|
+
status: 401
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (config.audience) {
|
|
1567
|
+
const audiences = Array.isArray(config.audience) ? config.audience : [config.audience];
|
|
1568
|
+
const tokenAudiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
|
|
1569
|
+
const hasValidAudience = audiences.some((aud) => tokenAudiences.includes(aud));
|
|
1570
|
+
if (!hasValidAudience) {
|
|
1571
|
+
return {
|
|
1572
|
+
code: "invalid_token",
|
|
1573
|
+
message: "Invalid token audience",
|
|
1574
|
+
status: 401
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
async function verifyJWT(token, config) {
|
|
1581
|
+
const decoded = decodeJWT(token);
|
|
1582
|
+
if (!decoded) {
|
|
1583
|
+
return {
|
|
1584
|
+
payload: null,
|
|
1585
|
+
error: {
|
|
1586
|
+
code: "invalid_token",
|
|
1587
|
+
message: "Malformed token",
|
|
1588
|
+
status: 401
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
const { header, payload, signature } = decoded;
|
|
1593
|
+
const alg = header.alg;
|
|
1594
|
+
const allowedAlgorithms = config.algorithms || ["HS256"];
|
|
1595
|
+
if (!allowedAlgorithms.includes(alg)) {
|
|
1596
|
+
return {
|
|
1597
|
+
payload: null,
|
|
1598
|
+
error: {
|
|
1599
|
+
code: "invalid_token",
|
|
1600
|
+
message: `Algorithm ${alg} not allowed`,
|
|
1601
|
+
status: 401
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
const algorithmParams = getAlgorithmParams(alg);
|
|
1606
|
+
if (!algorithmParams) {
|
|
1607
|
+
return {
|
|
1608
|
+
payload: null,
|
|
1609
|
+
error: {
|
|
1610
|
+
code: "invalid_token",
|
|
1611
|
+
message: `Unsupported algorithm: ${alg}`,
|
|
1612
|
+
status: 401
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
const parts = token.split(".");
|
|
1617
|
+
const signedData = `${parts[0]}.${parts[1]}`;
|
|
1618
|
+
let isValid = false;
|
|
1619
|
+
try {
|
|
1620
|
+
if (algorithmParams.name === "HMAC") {
|
|
1621
|
+
if (!config.secret) {
|
|
1622
|
+
return {
|
|
1623
|
+
payload: null,
|
|
1624
|
+
error: {
|
|
1625
|
+
code: "invalid_token",
|
|
1626
|
+
message: "Secret required for HMAC algorithms",
|
|
1627
|
+
status: 500
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
isValid = await verifyHMAC(signedData, signature, config.secret, algorithmParams.hash);
|
|
1632
|
+
} else {
|
|
1633
|
+
if (!config.publicKey) {
|
|
1634
|
+
return {
|
|
1635
|
+
payload: null,
|
|
1636
|
+
error: {
|
|
1637
|
+
code: "invalid_token",
|
|
1638
|
+
message: "Public key required for asymmetric algorithms",
|
|
1639
|
+
status: 500
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
isValid = await verifyAsymmetric(signedData, signature, config.publicKey, algorithmParams);
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
isValid = false;
|
|
1647
|
+
}
|
|
1648
|
+
if (!isValid) {
|
|
1649
|
+
return {
|
|
1650
|
+
payload: null,
|
|
1651
|
+
error: {
|
|
1652
|
+
code: "invalid_signature",
|
|
1653
|
+
message: "Invalid token signature",
|
|
1654
|
+
status: 401
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
const claimsError = validateClaims(payload, config);
|
|
1659
|
+
if (claimsError) {
|
|
1660
|
+
return { payload: null, error: claimsError };
|
|
1661
|
+
}
|
|
1662
|
+
return { payload, error: null };
|
|
1663
|
+
}
|
|
1664
|
+
function extractBearerToken(authHeader) {
|
|
1665
|
+
if (!authHeader) return null;
|
|
1666
|
+
if (!authHeader.startsWith("Bearer ")) return null;
|
|
1667
|
+
return authHeader.slice(7);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/middleware/auth/middleware.ts
|
|
1671
|
+
function defaultErrorResponse2(_req, error) {
|
|
1672
|
+
return new Response(
|
|
1673
|
+
JSON.stringify({
|
|
1674
|
+
error: error.code,
|
|
1675
|
+
message: error.message
|
|
1676
|
+
}),
|
|
1677
|
+
{
|
|
1678
|
+
status: error.status,
|
|
1679
|
+
headers: { "Content-Type": "application/json" }
|
|
1680
|
+
}
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
async function getTokenFromRequest(req, config) {
|
|
1684
|
+
if (config?.getToken) {
|
|
1685
|
+
return config.getToken(req);
|
|
1686
|
+
}
|
|
1687
|
+
return extractBearerToken(req.headers.get("authorization"));
|
|
1688
|
+
}
|
|
1689
|
+
function withJWT(handler, config) {
|
|
1690
|
+
const secret = config.secret || process.env.JWT_SECRET;
|
|
1691
|
+
const effectiveConfig = { ...config, secret };
|
|
1692
|
+
return async (req) => {
|
|
1693
|
+
const token = await getTokenFromRequest(req, effectiveConfig);
|
|
1694
|
+
if (!token) {
|
|
1695
|
+
return defaultErrorResponse2(req, {
|
|
1696
|
+
code: "missing_token",
|
|
1697
|
+
message: "Authentication required",
|
|
1698
|
+
status: 401
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
const { payload, error } = await verifyJWT(token, effectiveConfig);
|
|
1702
|
+
if (error) {
|
|
1703
|
+
return defaultErrorResponse2(req, error);
|
|
1704
|
+
}
|
|
1705
|
+
const user = effectiveConfig.mapUser ? await effectiveConfig.mapUser(payload) : {
|
|
1706
|
+
id: payload.sub || "",
|
|
1707
|
+
email: payload.email,
|
|
1708
|
+
name: payload.name,
|
|
1709
|
+
roles: payload.roles,
|
|
1710
|
+
permissions: payload.permissions
|
|
1711
|
+
};
|
|
1712
|
+
return handler(req, { user, token });
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function withAPIKey(handler, config) {
|
|
1716
|
+
const headerName = config.headerName || "x-api-key";
|
|
1717
|
+
const queryParam = config.queryParam || "api_key";
|
|
1718
|
+
return async (req) => {
|
|
1719
|
+
let apiKey = req.headers.get(headerName);
|
|
1720
|
+
if (!apiKey) {
|
|
1721
|
+
const url = new URL(req.url);
|
|
1722
|
+
apiKey = url.searchParams.get(queryParam);
|
|
1723
|
+
}
|
|
1724
|
+
if (!apiKey) {
|
|
1725
|
+
return defaultErrorResponse2(req, {
|
|
1726
|
+
code: "missing_api_key",
|
|
1727
|
+
message: "API key required",
|
|
1728
|
+
status: 401
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
const user = await config.validate(apiKey, req);
|
|
1732
|
+
if (!user) {
|
|
1733
|
+
return defaultErrorResponse2(req, {
|
|
1734
|
+
code: "invalid_api_key",
|
|
1735
|
+
message: "Invalid API key",
|
|
1736
|
+
status: 401
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
return handler(req, { user });
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function withSession(handler, config) {
|
|
1743
|
+
const cookieName = config.cookieName || "session";
|
|
1744
|
+
return async (req) => {
|
|
1745
|
+
const sessionId = req.cookies.get(cookieName)?.value;
|
|
1746
|
+
if (!sessionId) {
|
|
1747
|
+
return defaultErrorResponse2(req, {
|
|
1748
|
+
code: "missing_session",
|
|
1749
|
+
message: "Session required",
|
|
1750
|
+
status: 401
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
const user = await config.validate(sessionId, req);
|
|
1754
|
+
if (!user) {
|
|
1755
|
+
return defaultErrorResponse2(req, {
|
|
1756
|
+
code: "invalid_session",
|
|
1757
|
+
message: "Invalid or expired session",
|
|
1758
|
+
status: 401
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
return handler(req, { user });
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
function withRoles(handler, config) {
|
|
1765
|
+
return async (req, ctx) => {
|
|
1766
|
+
const { user } = ctx;
|
|
1767
|
+
const userRoles = config.getUserRoles ? config.getUserRoles(user) : user.roles || [];
|
|
1768
|
+
if (config.roles && config.roles.length > 0) {
|
|
1769
|
+
const hasRole = config.roles.some((role) => userRoles.includes(role));
|
|
1770
|
+
if (!hasRole) {
|
|
1771
|
+
return defaultErrorResponse2(req, {
|
|
1772
|
+
code: "insufficient_roles",
|
|
1773
|
+
message: "Insufficient permissions",
|
|
1774
|
+
status: 403
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const userPermissions = config.getUserPermissions ? config.getUserPermissions(user) : user.permissions || [];
|
|
1779
|
+
if (config.permissions && config.permissions.length > 0) {
|
|
1780
|
+
const hasAllPermissions = config.permissions.every(
|
|
1781
|
+
(perm) => userPermissions.includes(perm)
|
|
1782
|
+
);
|
|
1783
|
+
if (!hasAllPermissions) {
|
|
1784
|
+
return defaultErrorResponse2(req, {
|
|
1785
|
+
code: "insufficient_permissions",
|
|
1786
|
+
message: "Insufficient permissions",
|
|
1787
|
+
status: 403
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
if (config.authorize) {
|
|
1792
|
+
const authorized = await config.authorize(user, req);
|
|
1793
|
+
if (!authorized) {
|
|
1794
|
+
return defaultErrorResponse2(req, {
|
|
1795
|
+
code: "unauthorized",
|
|
1796
|
+
message: "Unauthorized",
|
|
1797
|
+
status: 403
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return handler(req, ctx);
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
function withAuth(handler, config) {
|
|
1805
|
+
const onError = config.onError || defaultErrorResponse2;
|
|
1806
|
+
return async (req) => {
|
|
1807
|
+
let user = null;
|
|
1808
|
+
let token;
|
|
1809
|
+
if (config.jwt) {
|
|
1810
|
+
const secret = config.jwt.secret || process.env.JWT_SECRET;
|
|
1811
|
+
const jwtConfig = { ...config.jwt, secret };
|
|
1812
|
+
const jwtToken = await getTokenFromRequest(req, jwtConfig);
|
|
1813
|
+
if (jwtToken) {
|
|
1814
|
+
const { payload, error } = await verifyJWT(jwtToken, jwtConfig);
|
|
1815
|
+
if (!error && payload) {
|
|
1816
|
+
user = jwtConfig.mapUser ? await jwtConfig.mapUser(payload) : {
|
|
1817
|
+
id: payload.sub || "",
|
|
1818
|
+
email: payload.email,
|
|
1819
|
+
name: payload.name,
|
|
1820
|
+
roles: payload.roles
|
|
1821
|
+
};
|
|
1822
|
+
token = jwtToken;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (!user && config.apiKey) {
|
|
1827
|
+
const headerName = config.apiKey.headerName || "x-api-key";
|
|
1828
|
+
const queryParam = config.apiKey.queryParam || "api_key";
|
|
1829
|
+
let apiKey = req.headers.get(headerName);
|
|
1830
|
+
if (!apiKey) {
|
|
1831
|
+
const url = new URL(req.url);
|
|
1832
|
+
apiKey = url.searchParams.get(queryParam);
|
|
1833
|
+
}
|
|
1834
|
+
if (apiKey) {
|
|
1835
|
+
const apiUser = await config.apiKey.validate(apiKey, req);
|
|
1836
|
+
if (apiUser) {
|
|
1837
|
+
user = apiUser;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (!user && config.session) {
|
|
1842
|
+
const cookieName = config.session.cookieName || "session";
|
|
1843
|
+
const sessionId = req.cookies.get(cookieName)?.value;
|
|
1844
|
+
if (sessionId) {
|
|
1845
|
+
const sessionUser = await config.session.validate(sessionId, req);
|
|
1846
|
+
if (sessionUser) {
|
|
1847
|
+
user = sessionUser;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (!user) {
|
|
1852
|
+
return onError(req, {
|
|
1853
|
+
code: "unauthorized",
|
|
1854
|
+
message: "Authentication required",
|
|
1855
|
+
status: 401
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
if (config.rbac) {
|
|
1859
|
+
const userRoles = config.rbac.getUserRoles ? config.rbac.getUserRoles(user) : user.roles || [];
|
|
1860
|
+
if (config.rbac.roles && config.rbac.roles.length > 0) {
|
|
1861
|
+
const hasRole = config.rbac.roles.some((role) => userRoles.includes(role));
|
|
1862
|
+
if (!hasRole) {
|
|
1863
|
+
return onError(req, {
|
|
1864
|
+
code: "insufficient_roles",
|
|
1865
|
+
message: "Insufficient permissions",
|
|
1866
|
+
status: 403
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const userPermissions = config.rbac.getUserPermissions ? config.rbac.getUserPermissions(user) : user.permissions || [];
|
|
1871
|
+
if (config.rbac.permissions && config.rbac.permissions.length > 0) {
|
|
1872
|
+
const hasAllPermissions = config.rbac.permissions.every(
|
|
1873
|
+
(perm) => userPermissions.includes(perm)
|
|
1874
|
+
);
|
|
1875
|
+
if (!hasAllPermissions) {
|
|
1876
|
+
return onError(req, {
|
|
1877
|
+
code: "insufficient_permissions",
|
|
1878
|
+
message: "Insufficient permissions",
|
|
1879
|
+
status: 403
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (config.rbac.authorize) {
|
|
1884
|
+
const authorized = await config.rbac.authorize(user, req);
|
|
1885
|
+
if (!authorized) {
|
|
1886
|
+
return onError(req, {
|
|
1887
|
+
code: "unauthorized",
|
|
1888
|
+
message: "Unauthorized",
|
|
1889
|
+
status: 403
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (config.onSuccess) {
|
|
1895
|
+
await config.onSuccess(req, user);
|
|
1896
|
+
}
|
|
1897
|
+
return handler(req, { user, token });
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function withOptionalAuth(handler, config) {
|
|
1901
|
+
return async (req) => {
|
|
1902
|
+
let user = null;
|
|
1903
|
+
let token;
|
|
1904
|
+
if (config.jwt) {
|
|
1905
|
+
const secret = config.jwt.secret || process.env.JWT_SECRET;
|
|
1906
|
+
const jwtConfig = { ...config.jwt, secret };
|
|
1907
|
+
const jwtToken = await getTokenFromRequest(req, jwtConfig);
|
|
1908
|
+
if (jwtToken) {
|
|
1909
|
+
const { payload, error } = await verifyJWT(jwtToken, jwtConfig);
|
|
1910
|
+
if (!error && payload) {
|
|
1911
|
+
user = jwtConfig.mapUser ? await jwtConfig.mapUser(payload) : {
|
|
1912
|
+
id: payload.sub || "",
|
|
1913
|
+
email: payload.email,
|
|
1914
|
+
name: payload.name,
|
|
1915
|
+
roles: payload.roles
|
|
1916
|
+
};
|
|
1917
|
+
token = jwtToken;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (!user && config.apiKey) {
|
|
1922
|
+
const headerName = config.apiKey.headerName || "x-api-key";
|
|
1923
|
+
let apiKey = req.headers.get(headerName);
|
|
1924
|
+
if (apiKey) {
|
|
1925
|
+
const apiUser = await config.apiKey.validate(apiKey, req);
|
|
1926
|
+
if (apiUser) user = apiUser;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (!user && config.session) {
|
|
1930
|
+
const cookieName = config.session.cookieName || "session";
|
|
1931
|
+
const sessionId = req.cookies.get(cookieName)?.value;
|
|
1932
|
+
if (sessionId) {
|
|
1933
|
+
const sessionUser = await config.session.validate(sessionId, req);
|
|
1934
|
+
if (sessionUser) user = sessionUser;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return handler(req, { user, token });
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/middleware/validation/utils.ts
|
|
1942
|
+
function isZodSchema(schema) {
|
|
1943
|
+
return typeof schema === "object" && schema !== null && "safeParse" in schema && typeof schema.safeParse === "function";
|
|
1944
|
+
}
|
|
1945
|
+
function isCustomSchema(schema) {
|
|
1946
|
+
if (typeof schema !== "object" || schema === null) return false;
|
|
1947
|
+
if ("safeParse" in schema) return false;
|
|
1948
|
+
const entries = Object.entries(schema);
|
|
1949
|
+
if (entries.length === 0) return false;
|
|
1950
|
+
return entries.every(([_, rule]) => {
|
|
1951
|
+
return typeof rule === "object" && rule !== null && "type" in rule;
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
var EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
1955
|
+
var URL_PATTERN = /^https?:\/\/(?:(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}|localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
|
|
1956
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1957
|
+
var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
1958
|
+
function validateField(value, rule, fieldName) {
|
|
1959
|
+
if (value === void 0 || value === null || value === "") {
|
|
1960
|
+
if (rule.required) {
|
|
1961
|
+
return {
|
|
1962
|
+
field: fieldName,
|
|
1963
|
+
code: "required",
|
|
1964
|
+
message: rule.message || `${fieldName} is required`,
|
|
1965
|
+
received: value
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
switch (rule.type) {
|
|
1971
|
+
case "string":
|
|
1972
|
+
if (typeof value !== "string") {
|
|
1973
|
+
return {
|
|
1974
|
+
field: fieldName,
|
|
1975
|
+
code: "invalid_type",
|
|
1976
|
+
message: rule.message || `${fieldName} must be a string`,
|
|
1977
|
+
expected: "string",
|
|
1978
|
+
received: typeof value
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
if (rule.minLength !== void 0 && value.length < rule.minLength) {
|
|
1982
|
+
return {
|
|
1983
|
+
field: fieldName,
|
|
1984
|
+
code: "too_short",
|
|
1985
|
+
message: rule.message || `${fieldName} must be at least ${rule.minLength} characters`,
|
|
1986
|
+
received: value.length
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
if (rule.maxLength !== void 0 && value.length > rule.maxLength) {
|
|
1990
|
+
return {
|
|
1991
|
+
field: fieldName,
|
|
1992
|
+
code: "too_long",
|
|
1993
|
+
message: rule.message || `${fieldName} must be at most ${rule.maxLength} characters`,
|
|
1994
|
+
received: value.length
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
1998
|
+
return {
|
|
1999
|
+
field: fieldName,
|
|
2000
|
+
code: "invalid_pattern",
|
|
2001
|
+
message: rule.message || `${fieldName} has invalid format`,
|
|
2002
|
+
received: value
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
break;
|
|
2006
|
+
case "number":
|
|
2007
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
2008
|
+
if (isNaN(num)) {
|
|
2009
|
+
return {
|
|
2010
|
+
field: fieldName,
|
|
2011
|
+
code: "invalid_type",
|
|
2012
|
+
message: rule.message || `${fieldName} must be a number`,
|
|
2013
|
+
expected: "number",
|
|
2014
|
+
received: typeof value
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
if (rule.integer && !Number.isInteger(num)) {
|
|
2018
|
+
return {
|
|
2019
|
+
field: fieldName,
|
|
2020
|
+
code: "invalid_integer",
|
|
2021
|
+
message: rule.message || `${fieldName} must be an integer`,
|
|
2022
|
+
received: num
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
if (rule.min !== void 0 && num < rule.min) {
|
|
2026
|
+
return {
|
|
2027
|
+
field: fieldName,
|
|
2028
|
+
code: "too_small",
|
|
2029
|
+
message: rule.message || `${fieldName} must be at least ${rule.min}`,
|
|
2030
|
+
received: num
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
if (rule.max !== void 0 && num > rule.max) {
|
|
2034
|
+
return {
|
|
2035
|
+
field: fieldName,
|
|
2036
|
+
code: "too_large",
|
|
2037
|
+
message: rule.message || `${fieldName} must be at most ${rule.max}`,
|
|
2038
|
+
received: num
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
break;
|
|
2042
|
+
case "boolean":
|
|
2043
|
+
if (typeof value !== "boolean" && value !== "true" && value !== "false") {
|
|
2044
|
+
return {
|
|
2045
|
+
field: fieldName,
|
|
2046
|
+
code: "invalid_type",
|
|
2047
|
+
message: rule.message || `${fieldName} must be a boolean`,
|
|
2048
|
+
expected: "boolean",
|
|
2049
|
+
received: typeof value
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
break;
|
|
2053
|
+
case "email":
|
|
2054
|
+
if (typeof value !== "string" || !EMAIL_PATTERN.test(value)) {
|
|
2055
|
+
return {
|
|
2056
|
+
field: fieldName,
|
|
2057
|
+
code: "invalid_email",
|
|
2058
|
+
message: rule.message || `${fieldName} must be a valid email address`,
|
|
2059
|
+
received: value
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
break;
|
|
2063
|
+
case "url":
|
|
2064
|
+
if (typeof value !== "string" || !URL_PATTERN.test(value)) {
|
|
2065
|
+
return {
|
|
2066
|
+
field: fieldName,
|
|
2067
|
+
code: "invalid_url",
|
|
2068
|
+
message: rule.message || `${fieldName} must be a valid URL`,
|
|
2069
|
+
received: value
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
break;
|
|
2073
|
+
case "uuid":
|
|
2074
|
+
if (typeof value !== "string" || !UUID_PATTERN.test(value)) {
|
|
2075
|
+
return {
|
|
2076
|
+
field: fieldName,
|
|
2077
|
+
code: "invalid_uuid",
|
|
2078
|
+
message: rule.message || `${fieldName} must be a valid UUID`,
|
|
2079
|
+
received: value
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
break;
|
|
2083
|
+
case "date":
|
|
2084
|
+
if (typeof value !== "string" || !DATE_PATTERN.test(value)) {
|
|
2085
|
+
const parsed = new Date(value);
|
|
2086
|
+
if (isNaN(parsed.getTime())) {
|
|
2087
|
+
return {
|
|
2088
|
+
field: fieldName,
|
|
2089
|
+
code: "invalid_date",
|
|
2090
|
+
message: rule.message || `${fieldName} must be a valid date`,
|
|
2091
|
+
received: value
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
break;
|
|
2096
|
+
case "array":
|
|
2097
|
+
if (!Array.isArray(value)) {
|
|
2098
|
+
return {
|
|
2099
|
+
field: fieldName,
|
|
2100
|
+
code: "invalid_type",
|
|
2101
|
+
message: rule.message || `${fieldName} must be an array`,
|
|
2102
|
+
expected: "array",
|
|
2103
|
+
received: typeof value
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
if (rule.minItems !== void 0 && value.length < rule.minItems) {
|
|
2107
|
+
return {
|
|
2108
|
+
field: fieldName,
|
|
2109
|
+
code: "too_few_items",
|
|
2110
|
+
message: rule.message || `${fieldName} must have at least ${rule.minItems} items`,
|
|
2111
|
+
received: value.length
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
if (rule.maxItems !== void 0 && value.length > rule.maxItems) {
|
|
2115
|
+
return {
|
|
2116
|
+
field: fieldName,
|
|
2117
|
+
code: "too_many_items",
|
|
2118
|
+
message: rule.message || `${fieldName} must have at most ${rule.maxItems} items`,
|
|
2119
|
+
received: value.length
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
if (rule.items) {
|
|
2123
|
+
for (let i = 0; i < value.length; i++) {
|
|
2124
|
+
const itemError = validateField(value[i], rule.items, `${fieldName}[${i}]`);
|
|
2125
|
+
if (itemError) return itemError;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
break;
|
|
2129
|
+
case "object":
|
|
2130
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2131
|
+
return {
|
|
2132
|
+
field: fieldName,
|
|
2133
|
+
code: "invalid_type",
|
|
2134
|
+
message: rule.message || `${fieldName} must be an object`,
|
|
2135
|
+
expected: "object",
|
|
2136
|
+
received: Array.isArray(value) ? "array" : typeof value
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
break;
|
|
2140
|
+
}
|
|
2141
|
+
if (rule.custom) {
|
|
2142
|
+
const result = rule.custom(value);
|
|
2143
|
+
if (result !== true) {
|
|
2144
|
+
return {
|
|
2145
|
+
field: fieldName,
|
|
2146
|
+
code: "custom_validation",
|
|
2147
|
+
message: typeof result === "string" ? result : rule.message || `${fieldName} failed validation`,
|
|
2148
|
+
received: value
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
function validateCustomSchema(data, schema) {
|
|
2155
|
+
if (typeof data !== "object" || data === null) {
|
|
2156
|
+
return {
|
|
2157
|
+
success: false,
|
|
2158
|
+
errors: [{
|
|
2159
|
+
field: "_root",
|
|
2160
|
+
code: "invalid_type",
|
|
2161
|
+
message: "Expected an object",
|
|
2162
|
+
received: data
|
|
2163
|
+
}]
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
const errors = [];
|
|
2167
|
+
const record = data;
|
|
2168
|
+
for (const [fieldName, rule] of Object.entries(schema)) {
|
|
2169
|
+
const error = validateField(record[fieldName], rule, fieldName);
|
|
2170
|
+
if (error) {
|
|
2171
|
+
errors.push(error);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (errors.length > 0) {
|
|
2175
|
+
return { success: false, errors };
|
|
2176
|
+
}
|
|
2177
|
+
return { success: true, data };
|
|
2178
|
+
}
|
|
2179
|
+
function validateZodSchema(data, schema) {
|
|
2180
|
+
const result = schema.safeParse(data);
|
|
2181
|
+
if (result.success) {
|
|
2182
|
+
return { success: true, data: result.data };
|
|
2183
|
+
}
|
|
2184
|
+
const errors = result.error.issues.map((issue) => ({
|
|
2185
|
+
field: issue.path.join(".") || "_root",
|
|
2186
|
+
code: issue.code,
|
|
2187
|
+
message: issue.message,
|
|
2188
|
+
path: issue.path.map(String)
|
|
2189
|
+
}));
|
|
2190
|
+
return { success: false, errors };
|
|
2191
|
+
}
|
|
2192
|
+
function walkObject(obj, fn, path = "") {
|
|
2193
|
+
if (typeof obj === "string") {
|
|
2194
|
+
return fn(obj, path);
|
|
2195
|
+
}
|
|
2196
|
+
if (Array.isArray(obj)) {
|
|
2197
|
+
return obj.map((item, i) => walkObject(item, fn, `${path}[${i}]`));
|
|
2198
|
+
}
|
|
2199
|
+
if (typeof obj === "object" && obj !== null) {
|
|
2200
|
+
const result = {};
|
|
2201
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2202
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
2203
|
+
result[key] = walkObject(value, fn, newPath);
|
|
2204
|
+
}
|
|
2205
|
+
return result;
|
|
2206
|
+
}
|
|
2207
|
+
return obj;
|
|
2208
|
+
}
|
|
2209
|
+
function parseQueryString(url) {
|
|
2210
|
+
const result = {};
|
|
2211
|
+
try {
|
|
2212
|
+
const urlObj = new URL(url);
|
|
2213
|
+
for (const [key, value] of urlObj.searchParams.entries()) {
|
|
2214
|
+
if (key in result) {
|
|
2215
|
+
const existing = result[key];
|
|
2216
|
+
if (Array.isArray(existing)) {
|
|
2217
|
+
existing.push(value);
|
|
2218
|
+
} else {
|
|
2219
|
+
result[key] = [existing, value];
|
|
2220
|
+
}
|
|
2221
|
+
} else {
|
|
2222
|
+
result[key] = value;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
} catch {
|
|
2226
|
+
}
|
|
2227
|
+
return result;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/middleware/validation/validators/schema.ts
|
|
2231
|
+
function validate(data, schema) {
|
|
2232
|
+
if (isZodSchema(schema)) {
|
|
2233
|
+
return validateZodSchema(data, schema);
|
|
2234
|
+
}
|
|
2235
|
+
if (isCustomSchema(schema)) {
|
|
2236
|
+
return validateCustomSchema(data, schema);
|
|
2237
|
+
}
|
|
2238
|
+
return {
|
|
2239
|
+
success: false,
|
|
2240
|
+
errors: [{
|
|
2241
|
+
field: "_schema",
|
|
2242
|
+
code: "invalid_schema",
|
|
2243
|
+
message: "Invalid schema provided"
|
|
2244
|
+
}]
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
async function validateBody(request, schema) {
|
|
2248
|
+
let body;
|
|
2249
|
+
try {
|
|
2250
|
+
const contentType = request.headers.get("content-type") || "";
|
|
2251
|
+
if (contentType.includes("application/json")) {
|
|
2252
|
+
body = await request.json();
|
|
2253
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
2254
|
+
const text = await request.text();
|
|
2255
|
+
body = Object.fromEntries(new URLSearchParams(text));
|
|
2256
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
2257
|
+
const formData = await request.formData();
|
|
2258
|
+
const obj = {};
|
|
2259
|
+
formData.forEach((value, key) => {
|
|
2260
|
+
if (typeof value === "string") {
|
|
2261
|
+
obj[key] = value;
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
body = obj;
|
|
2265
|
+
} else {
|
|
2266
|
+
try {
|
|
2267
|
+
body = await request.json();
|
|
2268
|
+
} catch {
|
|
2269
|
+
body = {};
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
return {
|
|
2274
|
+
success: false,
|
|
2275
|
+
errors: [{
|
|
2276
|
+
field: "_body",
|
|
2277
|
+
code: "parse_error",
|
|
2278
|
+
message: "Failed to parse request body"
|
|
2279
|
+
}]
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
return validate(body, schema);
|
|
2283
|
+
}
|
|
2284
|
+
function validateQuery(request, schema) {
|
|
2285
|
+
const query = parseQueryString(request.url);
|
|
2286
|
+
return validate(query, schema);
|
|
2287
|
+
}
|
|
2288
|
+
function validateParams(params, schema) {
|
|
2289
|
+
return validate(params, schema);
|
|
2290
|
+
}
|
|
2291
|
+
async function validateRequest(request, config) {
|
|
2292
|
+
const allErrors = [];
|
|
2293
|
+
const data = {};
|
|
2294
|
+
if (config.body) {
|
|
2295
|
+
const bodyResult = await validateBody(request, config.body);
|
|
2296
|
+
if (!bodyResult.success) {
|
|
2297
|
+
allErrors.push(...(bodyResult.errors || []).map((e) => ({
|
|
2298
|
+
...e,
|
|
2299
|
+
field: `body.${e.field}`.replace("body._root", "body")
|
|
2300
|
+
})));
|
|
2301
|
+
} else {
|
|
2302
|
+
data.body = bodyResult.data;
|
|
2303
|
+
}
|
|
2304
|
+
} else {
|
|
2305
|
+
data.body = {};
|
|
2306
|
+
}
|
|
2307
|
+
if (config.query) {
|
|
2308
|
+
const queryResult = validateQuery(request, config.query);
|
|
2309
|
+
if (!queryResult.success) {
|
|
2310
|
+
allErrors.push(...(queryResult.errors || []).map((e) => ({
|
|
2311
|
+
...e,
|
|
2312
|
+
field: `query.${e.field}`.replace("query._root", "query")
|
|
2313
|
+
})));
|
|
2314
|
+
} else {
|
|
2315
|
+
data.query = queryResult.data;
|
|
2316
|
+
}
|
|
2317
|
+
} else {
|
|
2318
|
+
data.query = {};
|
|
2319
|
+
}
|
|
2320
|
+
if (config.params && config.routeParams) {
|
|
2321
|
+
const paramsResult = validateParams(config.routeParams, config.params);
|
|
2322
|
+
if (!paramsResult.success) {
|
|
2323
|
+
allErrors.push(...(paramsResult.errors || []).map((e) => ({
|
|
2324
|
+
...e,
|
|
2325
|
+
field: `params.${e.field}`.replace("params._root", "params")
|
|
2326
|
+
})));
|
|
2327
|
+
} else {
|
|
2328
|
+
data.params = paramsResult.data;
|
|
2329
|
+
}
|
|
2330
|
+
} else {
|
|
2331
|
+
data.params = {};
|
|
2332
|
+
}
|
|
2333
|
+
if (allErrors.length > 0) {
|
|
2334
|
+
return { success: false, errors: allErrors };
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
success: true,
|
|
2338
|
+
data
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function defaultValidationErrorResponse(errors) {
|
|
2342
|
+
return new Response(
|
|
2343
|
+
JSON.stringify({
|
|
2344
|
+
error: "validation_error",
|
|
2345
|
+
message: "Request validation failed",
|
|
2346
|
+
details: errors.map((e) => ({
|
|
2347
|
+
field: e.field,
|
|
2348
|
+
code: e.code,
|
|
2349
|
+
message: e.message
|
|
2350
|
+
}))
|
|
2351
|
+
}),
|
|
2352
|
+
{
|
|
2353
|
+
status: 400,
|
|
2354
|
+
headers: { "Content-Type": "application/json" }
|
|
2355
|
+
}
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
function createValidator(schema) {
|
|
2359
|
+
return (data) => validate(data, schema);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/middleware/validation/validators/content-type.ts
|
|
2363
|
+
var MIME_TYPES = {
|
|
2364
|
+
// Text
|
|
2365
|
+
TEXT_PLAIN: "text/plain",
|
|
2366
|
+
TEXT_HTML: "text/html",
|
|
2367
|
+
TEXT_CSS: "text/css",
|
|
2368
|
+
TEXT_JAVASCRIPT: "text/javascript",
|
|
2369
|
+
// Application
|
|
2370
|
+
JSON: "application/json",
|
|
2371
|
+
FORM_URLENCODED: "application/x-www-form-urlencoded",
|
|
2372
|
+
MULTIPART_FORM: "multipart/form-data",
|
|
2373
|
+
XML: "application/xml",
|
|
2374
|
+
PDF: "application/pdf",
|
|
2375
|
+
ZIP: "application/zip",
|
|
2376
|
+
GZIP: "application/gzip",
|
|
2377
|
+
OCTET_STREAM: "application/octet-stream",
|
|
2378
|
+
// Image
|
|
2379
|
+
IMAGE_PNG: "image/png",
|
|
2380
|
+
IMAGE_JPEG: "image/jpeg",
|
|
2381
|
+
IMAGE_GIF: "image/gif",
|
|
2382
|
+
IMAGE_WEBP: "image/webp",
|
|
2383
|
+
IMAGE_SVG: "image/svg+xml",
|
|
2384
|
+
// Audio
|
|
2385
|
+
AUDIO_MP3: "audio/mpeg",
|
|
2386
|
+
AUDIO_WAV: "audio/wav",
|
|
2387
|
+
AUDIO_OGG: "audio/ogg",
|
|
2388
|
+
// Video
|
|
2389
|
+
VIDEO_MP4: "video/mp4",
|
|
2390
|
+
VIDEO_WEBM: "video/webm"
|
|
2391
|
+
};
|
|
2392
|
+
function parseContentType(header) {
|
|
2393
|
+
if (!header) {
|
|
2394
|
+
return {
|
|
2395
|
+
type: "",
|
|
2396
|
+
subtype: "",
|
|
2397
|
+
mediaType: "",
|
|
2398
|
+
parameters: {}
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
const parts = header.split(";").map((p) => p.trim());
|
|
2402
|
+
const mediaType = parts[0].toLowerCase();
|
|
2403
|
+
const [type = "", subtype = ""] = mediaType.split("/");
|
|
2404
|
+
const parameters = {};
|
|
2405
|
+
for (let i = 1; i < parts.length; i++) {
|
|
2406
|
+
const [key, value] = parts[i].split("=").map((p) => p.trim());
|
|
2407
|
+
if (key && value) {
|
|
2408
|
+
parameters[key.toLowerCase()] = value.replace(/^["']|["']$/g, "");
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return {
|
|
2412
|
+
type,
|
|
2413
|
+
subtype,
|
|
2414
|
+
mediaType,
|
|
2415
|
+
charset: parameters["charset"],
|
|
2416
|
+
boundary: parameters["boundary"],
|
|
2417
|
+
parameters
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
function isAllowedContentType(contentType, allowedTypes, strict = false) {
|
|
2421
|
+
if (!contentType) {
|
|
2422
|
+
return !strict;
|
|
2423
|
+
}
|
|
2424
|
+
const { mediaType } = parseContentType(contentType);
|
|
2425
|
+
return allowedTypes.some((allowed) => {
|
|
2426
|
+
const normalizedAllowed = allowed.toLowerCase().trim();
|
|
2427
|
+
if (mediaType === normalizedAllowed) {
|
|
2428
|
+
return true;
|
|
2429
|
+
}
|
|
2430
|
+
if (normalizedAllowed.endsWith("/*")) {
|
|
2431
|
+
const prefix = normalizedAllowed.slice(0, -2);
|
|
2432
|
+
return mediaType.startsWith(prefix + "/");
|
|
2433
|
+
}
|
|
2434
|
+
if (!normalizedAllowed.includes("/")) {
|
|
2435
|
+
const { type } = parseContentType(contentType);
|
|
2436
|
+
return type === normalizedAllowed;
|
|
2437
|
+
}
|
|
2438
|
+
return false;
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
function validateContentType(request, config) {
|
|
2442
|
+
const contentType = request.headers.get("content-type");
|
|
2443
|
+
const { allowed, strict = false, charset } = config;
|
|
2444
|
+
if (strict && !contentType) {
|
|
2445
|
+
return {
|
|
2446
|
+
valid: false,
|
|
2447
|
+
contentType: null,
|
|
2448
|
+
reason: "Content-Type header is required"
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
if (contentType && !isAllowedContentType(contentType, allowed, strict)) {
|
|
2452
|
+
return {
|
|
2453
|
+
valid: false,
|
|
2454
|
+
contentType,
|
|
2455
|
+
reason: `Content-Type '${contentType}' is not allowed`
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
if (charset && contentType) {
|
|
2459
|
+
const parsed = parseContentType(contentType);
|
|
2460
|
+
if (parsed.charset && parsed.charset.toLowerCase() !== charset.toLowerCase()) {
|
|
2461
|
+
return {
|
|
2462
|
+
valid: false,
|
|
2463
|
+
contentType,
|
|
2464
|
+
reason: `Charset '${parsed.charset}' is not allowed, expected '${charset}'`
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return { valid: true, contentType };
|
|
2469
|
+
}
|
|
2470
|
+
function defaultContentTypeErrorResponse(contentType, reason) {
|
|
2471
|
+
return new Response(
|
|
2472
|
+
JSON.stringify({
|
|
2473
|
+
error: "invalid_content_type",
|
|
2474
|
+
message: reason,
|
|
2475
|
+
received: contentType
|
|
2476
|
+
}),
|
|
2477
|
+
{
|
|
2478
|
+
status: 415,
|
|
2479
|
+
// Unsupported Media Type
|
|
2480
|
+
headers: { "Content-Type": "application/json" }
|
|
2481
|
+
}
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
function isJsonRequest(request) {
|
|
2485
|
+
return isAllowedContentType(
|
|
2486
|
+
request.headers.get("content-type"),
|
|
2487
|
+
[MIME_TYPES.JSON]
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
function isFormRequest(request) {
|
|
2491
|
+
return isAllowedContentType(
|
|
2492
|
+
request.headers.get("content-type"),
|
|
2493
|
+
[MIME_TYPES.FORM_URLENCODED, MIME_TYPES.MULTIPART_FORM]
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// src/middleware/validation/sanitizers/path.ts
|
|
2498
|
+
var DANGEROUS_PATTERNS = [
|
|
2499
|
+
// Unix path traversal
|
|
2500
|
+
/\.\.\//g,
|
|
2501
|
+
/\.\./g,
|
|
2502
|
+
// Windows path traversal
|
|
2503
|
+
/\.\.\\/g,
|
|
2504
|
+
// Null byte (can truncate paths in some systems)
|
|
2505
|
+
/%00/g,
|
|
2506
|
+
/\0/g,
|
|
2507
|
+
// URL encoded traversal
|
|
2508
|
+
/%2e%2e%2f/gi,
|
|
2509
|
+
// ../
|
|
2510
|
+
/%2e%2e\//gi,
|
|
2511
|
+
// ../
|
|
2512
|
+
/%2e%2e%5c/gi,
|
|
2513
|
+
// ..\
|
|
2514
|
+
/%2e%2e\\/gi,
|
|
2515
|
+
// ..\
|
|
2516
|
+
// Double URL encoding
|
|
2517
|
+
/%252e%252e%252f/gi,
|
|
2518
|
+
/%252e%252e%255c/gi,
|
|
2519
|
+
// Unicode encoding
|
|
2520
|
+
/\.%u002e\//gi,
|
|
2521
|
+
/%u002e%u002e%u002f/gi,
|
|
2522
|
+
// Overlong UTF-8 encoding
|
|
2523
|
+
/%c0%ae%c0%ae%c0%af/gi,
|
|
2524
|
+
/%c1%9c/gi
|
|
2525
|
+
// Backslash variant
|
|
2526
|
+
];
|
|
2527
|
+
var DEFAULT_BLOCKED_EXTENSIONS = [
|
|
2528
|
+
".exe",
|
|
2529
|
+
".dll",
|
|
2530
|
+
".so",
|
|
2531
|
+
".dylib",
|
|
2532
|
+
// Executables
|
|
2533
|
+
".sh",
|
|
2534
|
+
".bash",
|
|
2535
|
+
".bat",
|
|
2536
|
+
".cmd",
|
|
2537
|
+
".ps1",
|
|
2538
|
+
// Scripts
|
|
2539
|
+
".php",
|
|
2540
|
+
".asp",
|
|
2541
|
+
".aspx",
|
|
2542
|
+
".jsp",
|
|
2543
|
+
".cgi",
|
|
2544
|
+
// Server scripts
|
|
2545
|
+
".htaccess",
|
|
2546
|
+
".htpasswd",
|
|
2547
|
+
// Apache config
|
|
2548
|
+
".env",
|
|
2549
|
+
".git",
|
|
2550
|
+
".svn"
|
|
2551
|
+
// Config/VCS
|
|
2552
|
+
];
|
|
2553
|
+
function normalizePathSeparators(path) {
|
|
2554
|
+
return path.replace(/\\/g, "/");
|
|
2555
|
+
}
|
|
2556
|
+
function decodePathComponent(path) {
|
|
2557
|
+
let result = path;
|
|
2558
|
+
let previous = "";
|
|
2559
|
+
while (result !== previous) {
|
|
2560
|
+
previous = result;
|
|
2561
|
+
try {
|
|
2562
|
+
result = decodeURIComponent(result);
|
|
2563
|
+
} catch {
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return result;
|
|
2568
|
+
}
|
|
2569
|
+
function hasPathTraversal(path) {
|
|
2570
|
+
if (!path || typeof path !== "string") return false;
|
|
2571
|
+
const normalized = normalizePathSeparators(decodePathComponent(path));
|
|
2572
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
2573
|
+
pattern.lastIndex = 0;
|
|
2574
|
+
if (pattern.test(normalized)) {
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
if (normalized.includes("..")) {
|
|
2579
|
+
return true;
|
|
2580
|
+
}
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
function validatePath(path, config = {}) {
|
|
2584
|
+
if (!path || typeof path !== "string") {
|
|
2585
|
+
return { valid: false, reason: "Path is empty or not a string" };
|
|
2586
|
+
}
|
|
2587
|
+
const {
|
|
2588
|
+
allowAbsolute = false,
|
|
2589
|
+
allowedPrefixes = [],
|
|
2590
|
+
allowedExtensions,
|
|
2591
|
+
blockedExtensions = DEFAULT_BLOCKED_EXTENSIONS,
|
|
2592
|
+
maxDepth = 10,
|
|
2593
|
+
maxLength = 255,
|
|
2594
|
+
normalize = true
|
|
2595
|
+
} = config;
|
|
2596
|
+
if (path.length > maxLength) {
|
|
2597
|
+
return { valid: false, reason: `Path exceeds maximum length of ${maxLength}` };
|
|
2598
|
+
}
|
|
2599
|
+
let normalized = decodePathComponent(path);
|
|
2600
|
+
if (normalize) {
|
|
2601
|
+
normalized = normalizePathSeparators(normalized);
|
|
2602
|
+
}
|
|
2603
|
+
if (normalized.includes("\0") || path.includes("%00")) {
|
|
2604
|
+
return { valid: false, reason: "Path contains null bytes" };
|
|
2605
|
+
}
|
|
2606
|
+
if (hasPathTraversal(path)) {
|
|
2607
|
+
return { valid: false, reason: "Path contains traversal sequences" };
|
|
2608
|
+
}
|
|
2609
|
+
const isAbsolute = normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || // Windows drive letter
|
|
2610
|
+
normalized.startsWith("\\\\");
|
|
2611
|
+
if (isAbsolute && !allowAbsolute) {
|
|
2612
|
+
return { valid: false, reason: "Absolute paths are not allowed" };
|
|
2613
|
+
}
|
|
2614
|
+
if (allowedPrefixes.length > 0) {
|
|
2615
|
+
const hasValidPrefix = allowedPrefixes.some((prefix) => {
|
|
2616
|
+
const normalizedPrefix = normalizePathSeparators(prefix);
|
|
2617
|
+
return normalized.startsWith(normalizedPrefix);
|
|
2618
|
+
});
|
|
2619
|
+
if (!hasValidPrefix) {
|
|
2620
|
+
return { valid: false, reason: "Path does not start with an allowed prefix" };
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
const segments = normalized.split("/").filter((s) => s && s !== ".");
|
|
2624
|
+
if (segments.length > maxDepth) {
|
|
2625
|
+
return { valid: false, reason: `Path depth exceeds maximum of ${maxDepth}` };
|
|
2626
|
+
}
|
|
2627
|
+
const lastSegment = segments[segments.length - 1] || "";
|
|
2628
|
+
const dotIndex = lastSegment.lastIndexOf(".");
|
|
2629
|
+
const extension = dotIndex > 0 ? lastSegment.slice(dotIndex).toLowerCase() : "";
|
|
2630
|
+
if (extension && blockedExtensions.length > 0) {
|
|
2631
|
+
if (blockedExtensions.map((e) => e.toLowerCase()).includes(extension)) {
|
|
2632
|
+
return { valid: false, reason: `Extension ${extension} is not allowed` };
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
if (extension && allowedExtensions && allowedExtensions.length > 0) {
|
|
2636
|
+
if (!allowedExtensions.map((e) => e.toLowerCase()).includes(extension)) {
|
|
2637
|
+
return { valid: false, reason: `Extension ${extension} is not in allowed list` };
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
const sanitized = normalized.replace(/\/+/g, "/");
|
|
2641
|
+
return { valid: true, sanitized };
|
|
2642
|
+
}
|
|
2643
|
+
function sanitizePath(path, config = {}) {
|
|
2644
|
+
if (!path || typeof path !== "string") return "";
|
|
2645
|
+
const { normalize = true, maxLength = 255 } = config;
|
|
2646
|
+
let result = decodePathComponent(path);
|
|
2647
|
+
if (normalize) {
|
|
2648
|
+
result = normalizePathSeparators(result);
|
|
2649
|
+
}
|
|
2650
|
+
result = result.replace(/\0/g, "").replace(/%00/g, "");
|
|
2651
|
+
result = result.replace(/\.\.\//g, "").replace(/\.\.\\/g, "");
|
|
2652
|
+
if (!config.allowAbsolute) {
|
|
2653
|
+
result = result.replace(/^\/+/, "");
|
|
2654
|
+
result = result.replace(/^[a-zA-Z]:/, "");
|
|
2655
|
+
result = result.replace(/^\\\\/, "");
|
|
2656
|
+
}
|
|
2657
|
+
result = result.replace(/\/+/g, "/");
|
|
2658
|
+
result = result.replace(/\/+$/, "");
|
|
2659
|
+
if (result.length > maxLength) {
|
|
2660
|
+
result = result.slice(0, maxLength);
|
|
2661
|
+
}
|
|
2662
|
+
return result;
|
|
2663
|
+
}
|
|
2664
|
+
function getExtension(path) {
|
|
2665
|
+
if (!path || typeof path !== "string") return "";
|
|
2666
|
+
const normalized = normalizePathSeparators(path);
|
|
2667
|
+
const segments = normalized.split("/");
|
|
2668
|
+
const filename = segments[segments.length - 1] || "";
|
|
2669
|
+
const dotIndex = filename.lastIndexOf(".");
|
|
2670
|
+
if (dotIndex <= 0) return "";
|
|
2671
|
+
return filename.slice(dotIndex).toLowerCase();
|
|
2672
|
+
}
|
|
2673
|
+
function sanitizeFilename(filename) {
|
|
2674
|
+
if (typeof filename !== "string") return "file";
|
|
2675
|
+
if (!filename) return "file";
|
|
2676
|
+
let result = filename;
|
|
2677
|
+
result = result.replace(/[/\\]/g, "");
|
|
2678
|
+
result = result.replace(/\0/g, "");
|
|
2679
|
+
result = result.replace(/[\x00-\x1f\x7f]/g, "");
|
|
2680
|
+
result = result.replace(/[<>:"|?*]/g, "");
|
|
2681
|
+
result = result.replace(/^[.\s]+|[.\s]+$/g, "");
|
|
2682
|
+
if (result.length > 255) {
|
|
2683
|
+
const ext = getExtension(result);
|
|
2684
|
+
const name = result.slice(0, 255 - ext.length);
|
|
2685
|
+
result = name + ext;
|
|
2686
|
+
}
|
|
2687
|
+
return result || "file";
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// src/middleware/validation/validators/file.ts
|
|
2691
|
+
var MAGIC_NUMBERS = [
|
|
2692
|
+
// Images
|
|
2693
|
+
{ type: "image/jpeg", extension: ".jpg", signature: [255, 216, 255] },
|
|
2694
|
+
{ type: "image/png", extension: ".png", signature: [137, 80, 78, 71, 13, 10, 26, 10] },
|
|
2695
|
+
{ type: "image/gif", extension: ".gif", signature: [71, 73, 70, 56] },
|
|
2696
|
+
// GIF87a or GIF89a
|
|
2697
|
+
{ type: "image/webp", extension: ".webp", signature: [82, 73, 70, 70], offset: 0 },
|
|
2698
|
+
// RIFF
|
|
2699
|
+
{ type: "image/bmp", extension: ".bmp", signature: [66, 77] },
|
|
2700
|
+
{ type: "image/tiff", extension: ".tiff", signature: [73, 73, 42, 0] },
|
|
2701
|
+
// Little endian
|
|
2702
|
+
{ type: "image/tiff", extension: ".tiff", signature: [77, 77, 0, 42] },
|
|
2703
|
+
// Big endian
|
|
2704
|
+
{ type: "image/x-icon", extension: ".ico", signature: [0, 0, 1, 0] },
|
|
2705
|
+
{ type: "image/svg+xml", extension: ".svg", signature: [60, 63, 120, 109, 108] },
|
|
2706
|
+
// <?xml
|
|
2707
|
+
// Documents
|
|
2708
|
+
{ type: "application/pdf", extension: ".pdf", signature: [37, 80, 68, 70] },
|
|
2709
|
+
// %PDF
|
|
2710
|
+
{ type: "application/zip", extension: ".zip", signature: [80, 75, 3, 4] },
|
|
2711
|
+
// PK
|
|
2712
|
+
{ type: "application/gzip", extension: ".gz", signature: [31, 139] },
|
|
2713
|
+
{ type: "application/x-rar-compressed", extension: ".rar", signature: [82, 97, 114, 33] },
|
|
2714
|
+
{ type: "application/x-7z-compressed", extension: ".7z", signature: [55, 122, 188, 175, 39, 28] },
|
|
2715
|
+
// Microsoft Office (new format - zip based)
|
|
2716
|
+
{ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", extension: ".xlsx", signature: [80, 75, 3, 4] },
|
|
2717
|
+
{ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", extension: ".docx", signature: [80, 75, 3, 4] },
|
|
2718
|
+
{ type: "application/vnd.openxmlformats-officedocument.presentationml.presentation", extension: ".pptx", signature: [80, 75, 3, 4] },
|
|
2719
|
+
// Microsoft Office (old format)
|
|
2720
|
+
{ type: "application/msword", extension: ".doc", signature: [208, 207, 17, 224, 161, 177, 26, 225] },
|
|
2721
|
+
{ type: "application/vnd.ms-excel", extension: ".xls", signature: [208, 207, 17, 224, 161, 177, 26, 225] },
|
|
2722
|
+
// Audio
|
|
2723
|
+
{ type: "audio/mpeg", extension: ".mp3", signature: [255, 251] },
|
|
2724
|
+
// MP3 frame sync
|
|
2725
|
+
{ type: "audio/mpeg", extension: ".mp3", signature: [73, 68, 51] },
|
|
2726
|
+
// ID3
|
|
2727
|
+
{ type: "audio/wav", extension: ".wav", signature: [82, 73, 70, 70] },
|
|
2728
|
+
// RIFF
|
|
2729
|
+
{ type: "audio/ogg", extension: ".ogg", signature: [79, 103, 103, 83] },
|
|
2730
|
+
{ type: "audio/flac", extension: ".flac", signature: [102, 76, 97, 67] },
|
|
2731
|
+
// Video
|
|
2732
|
+
{ type: "video/mp4", extension: ".mp4", signature: [0, 0, 0], offset: 0 },
|
|
2733
|
+
// Partial match
|
|
2734
|
+
{ type: "video/webm", extension: ".webm", signature: [26, 69, 223, 163] },
|
|
2735
|
+
{ type: "video/avi", extension: ".avi", signature: [82, 73, 70, 70] },
|
|
2736
|
+
// RIFF
|
|
2737
|
+
{ type: "video/quicktime", extension: ".mov", signature: [0, 0, 0, 20, 102, 116, 121, 112] },
|
|
2738
|
+
// Web
|
|
2739
|
+
{ type: "application/wasm", extension: ".wasm", signature: [0, 97, 115, 109] },
|
|
2740
|
+
// \0asm
|
|
2741
|
+
// Fonts
|
|
2742
|
+
{ type: "font/woff", extension: ".woff", signature: [119, 79, 70, 70] },
|
|
2743
|
+
{ type: "font/woff2", extension: ".woff2", signature: [119, 79, 70, 50] }
|
|
2744
|
+
];
|
|
2745
|
+
var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
2746
|
+
var DEFAULT_MAX_FILES = 10;
|
|
2747
|
+
var DANGEROUS_EXTENSIONS = [
|
|
2748
|
+
".exe",
|
|
2749
|
+
".dll",
|
|
2750
|
+
".so",
|
|
2751
|
+
".dylib",
|
|
2752
|
+
".bin",
|
|
2753
|
+
".sh",
|
|
2754
|
+
".bash",
|
|
2755
|
+
".bat",
|
|
2756
|
+
".cmd",
|
|
2757
|
+
".ps1",
|
|
2758
|
+
".vbs",
|
|
2759
|
+
".php",
|
|
2760
|
+
".asp",
|
|
2761
|
+
".aspx",
|
|
2762
|
+
".jsp",
|
|
2763
|
+
".cgi",
|
|
2764
|
+
".pl",
|
|
2765
|
+
".py",
|
|
2766
|
+
".rb",
|
|
2767
|
+
".jar",
|
|
2768
|
+
".class",
|
|
2769
|
+
".msi",
|
|
2770
|
+
".dmg",
|
|
2771
|
+
".pkg",
|
|
2772
|
+
".deb",
|
|
2773
|
+
".rpm",
|
|
2774
|
+
".scr",
|
|
2775
|
+
".pif",
|
|
2776
|
+
".com",
|
|
2777
|
+
".hta"
|
|
2778
|
+
];
|
|
2779
|
+
function checkMagicNumber(bytes, magicNumber) {
|
|
2780
|
+
const offset = magicNumber.offset || 0;
|
|
2781
|
+
const signature = magicNumber.signature;
|
|
2782
|
+
if (bytes.length < offset + signature.length) {
|
|
2783
|
+
return false;
|
|
2784
|
+
}
|
|
2785
|
+
for (let i = 0; i < signature.length; i++) {
|
|
2786
|
+
if (bytes[offset + i] !== signature[i]) {
|
|
2787
|
+
return false;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2792
|
+
function detectFileType(bytes) {
|
|
2793
|
+
for (const magic of MAGIC_NUMBERS) {
|
|
2794
|
+
if (checkMagicNumber(bytes, magic)) {
|
|
2795
|
+
return { type: magic.type, extension: magic.extension };
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
return null;
|
|
2799
|
+
}
|
|
2800
|
+
async function validateFile(file, config = {}) {
|
|
2801
|
+
const {
|
|
2802
|
+
maxSize = DEFAULT_MAX_FILE_SIZE,
|
|
2803
|
+
minSize = 0,
|
|
2804
|
+
allowedTypes = [],
|
|
2805
|
+
blockedTypes = [],
|
|
2806
|
+
allowedExtensions = [],
|
|
2807
|
+
blockedExtensions = DANGEROUS_EXTENSIONS,
|
|
2808
|
+
validateMagicNumbers = true,
|
|
2809
|
+
sanitizeFilename: doSanitize = true
|
|
2810
|
+
} = config;
|
|
2811
|
+
const errors = [];
|
|
2812
|
+
const extension = getExtension(file.name);
|
|
2813
|
+
const info = {
|
|
2814
|
+
filename: doSanitize ? sanitizeFilename(file.name) : file.name,
|
|
2815
|
+
size: file.size,
|
|
2816
|
+
type: file.type,
|
|
2817
|
+
extension
|
|
2818
|
+
};
|
|
2819
|
+
if (file.size > maxSize) {
|
|
2820
|
+
errors.push({
|
|
2821
|
+
filename: file.name,
|
|
2822
|
+
code: "size_exceeded",
|
|
2823
|
+
message: `File size (${formatBytes(file.size)}) exceeds maximum allowed (${formatBytes(maxSize)})`,
|
|
2824
|
+
details: { size: file.size, maxSize }
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
if (file.size < minSize) {
|
|
2828
|
+
errors.push({
|
|
2829
|
+
filename: file.name,
|
|
2830
|
+
code: "size_too_small",
|
|
2831
|
+
message: `File size (${formatBytes(file.size)}) is below minimum required (${formatBytes(minSize)})`,
|
|
2832
|
+
details: { size: file.size, minSize }
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
if (blockedExtensions.length > 0 && extension) {
|
|
2836
|
+
if (blockedExtensions.map((e) => e.toLowerCase()).includes(extension.toLowerCase())) {
|
|
2837
|
+
errors.push({
|
|
2838
|
+
filename: file.name,
|
|
2839
|
+
code: "extension_not_allowed",
|
|
2840
|
+
message: `File extension '${extension}' is not allowed`,
|
|
2841
|
+
details: { extension, blockedExtensions }
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
if (allowedExtensions.length > 0 && extension) {
|
|
2846
|
+
if (!allowedExtensions.map((e) => e.toLowerCase()).includes(extension.toLowerCase())) {
|
|
2847
|
+
errors.push({
|
|
2848
|
+
filename: file.name,
|
|
2849
|
+
code: "extension_not_allowed",
|
|
2850
|
+
message: `File extension '${extension}' is not in allowed list`,
|
|
2851
|
+
details: { extension, allowedExtensions }
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
if (blockedTypes.length > 0 && file.type) {
|
|
2856
|
+
if (blockedTypes.includes(file.type)) {
|
|
2857
|
+
errors.push({
|
|
2858
|
+
filename: file.name,
|
|
2859
|
+
code: "type_not_allowed",
|
|
2860
|
+
message: `File type '${file.type}' is not allowed`,
|
|
2861
|
+
details: { type: file.type, blockedTypes }
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
if (allowedTypes.length > 0) {
|
|
2866
|
+
if (!allowedTypes.includes(file.type)) {
|
|
2867
|
+
errors.push({
|
|
2868
|
+
filename: file.name,
|
|
2869
|
+
code: "type_not_allowed",
|
|
2870
|
+
message: `File type '${file.type}' is not in allowed list`,
|
|
2871
|
+
details: { type: file.type, allowedTypes }
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (validateMagicNumbers && errors.length === 0) {
|
|
2876
|
+
try {
|
|
2877
|
+
const buffer = await file.arrayBuffer();
|
|
2878
|
+
const bytes = new Uint8Array(buffer.slice(0, 32));
|
|
2879
|
+
const detected = detectFileType(bytes);
|
|
2880
|
+
if (detected) {
|
|
2881
|
+
if (file.type && detected.type !== file.type) {
|
|
2882
|
+
const isSimilar = detected.type.startsWith("image/") && file.type.startsWith("image/") || detected.type.startsWith("audio/") && file.type.startsWith("audio/") || detected.type.startsWith("video/") && file.type.startsWith("video/");
|
|
2883
|
+
if (!isSimilar) {
|
|
2884
|
+
errors.push({
|
|
2885
|
+
filename: file.name,
|
|
2886
|
+
code: "invalid_content",
|
|
2887
|
+
message: `File content doesn't match declared type (claimed: ${file.type}, detected: ${detected.type})`,
|
|
2888
|
+
details: { claimed: file.type, detected: detected.type }
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
return {
|
|
2897
|
+
valid: errors.length === 0,
|
|
2898
|
+
info,
|
|
2899
|
+
errors
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
async function validateFiles(files, config = {}) {
|
|
2903
|
+
const { maxFiles = DEFAULT_MAX_FILES } = config;
|
|
2904
|
+
const allErrors = [];
|
|
2905
|
+
const infos = [];
|
|
2906
|
+
if (files.length > maxFiles) {
|
|
2907
|
+
allErrors.push({
|
|
2908
|
+
filename: "",
|
|
2909
|
+
code: "too_many_files",
|
|
2910
|
+
message: `Too many files (${files.length}), maximum allowed is ${maxFiles}`,
|
|
2911
|
+
details: { count: files.length, maxFiles }
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
for (const file of files) {
|
|
2915
|
+
const result = await validateFile(file, config);
|
|
2916
|
+
infos.push(result.info);
|
|
2917
|
+
allErrors.push(...result.errors);
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
valid: allErrors.length === 0,
|
|
2921
|
+
infos,
|
|
2922
|
+
errors: allErrors
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
function extractFilesFromFormData(formData) {
|
|
2926
|
+
const files = /* @__PURE__ */ new Map();
|
|
2927
|
+
formData.forEach((value, key) => {
|
|
2928
|
+
if (value instanceof File) {
|
|
2929
|
+
const existing = files.get(key) || [];
|
|
2930
|
+
existing.push(value);
|
|
2931
|
+
files.set(key, existing);
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
return files;
|
|
2935
|
+
}
|
|
2936
|
+
async function validateFilesFromRequest(request, config = {}) {
|
|
2937
|
+
const contentType = request.headers.get("content-type") || "";
|
|
2938
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
2939
|
+
return { valid: true, files: /* @__PURE__ */ new Map(), errors: [] };
|
|
2940
|
+
}
|
|
2941
|
+
try {
|
|
2942
|
+
const formData = await request.formData();
|
|
2943
|
+
const fileMap = extractFilesFromFormData(formData);
|
|
2944
|
+
const allInfos = /* @__PURE__ */ new Map();
|
|
2945
|
+
const allErrors = [];
|
|
2946
|
+
let totalFileCount = 0;
|
|
2947
|
+
for (const [field, files] of fileMap.entries()) {
|
|
2948
|
+
totalFileCount += files.length;
|
|
2949
|
+
const result = await validateFiles(files, { ...config, maxFiles: Infinity });
|
|
2950
|
+
allInfos.set(field, result.infos);
|
|
2951
|
+
allErrors.push(...result.errors.map((e) => ({ ...e, field })));
|
|
2952
|
+
}
|
|
2953
|
+
const maxFiles = config.maxFiles ?? DEFAULT_MAX_FILES;
|
|
2954
|
+
if (totalFileCount > maxFiles) {
|
|
2955
|
+
allErrors.push({
|
|
2956
|
+
filename: "",
|
|
2957
|
+
code: "too_many_files",
|
|
2958
|
+
message: `Total file count (${totalFileCount}) exceeds maximum (${maxFiles})`,
|
|
2959
|
+
details: { count: totalFileCount, maxFiles }
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
return {
|
|
2963
|
+
valid: allErrors.length === 0,
|
|
2964
|
+
files: allInfos,
|
|
2965
|
+
errors: allErrors
|
|
2966
|
+
};
|
|
2967
|
+
} catch {
|
|
2968
|
+
return {
|
|
2969
|
+
valid: false,
|
|
2970
|
+
files: /* @__PURE__ */ new Map(),
|
|
2971
|
+
errors: [{
|
|
2972
|
+
filename: "",
|
|
2973
|
+
code: "invalid_content",
|
|
2974
|
+
message: "Failed to parse multipart form data"
|
|
2975
|
+
}]
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
function defaultFileErrorResponse(errors) {
|
|
2980
|
+
return new Response(
|
|
2981
|
+
JSON.stringify({
|
|
2982
|
+
error: "file_validation_error",
|
|
2983
|
+
message: "File validation failed",
|
|
2984
|
+
details: errors.map((e) => ({
|
|
2985
|
+
filename: e.filename,
|
|
2986
|
+
field: e.field,
|
|
2987
|
+
code: e.code,
|
|
2988
|
+
message: e.message
|
|
2989
|
+
}))
|
|
2990
|
+
}),
|
|
2991
|
+
{
|
|
2992
|
+
status: 400,
|
|
2993
|
+
headers: { "Content-Type": "application/json" }
|
|
2994
|
+
}
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
function formatBytes(bytes) {
|
|
2998
|
+
if (bytes === 0) return "0 B";
|
|
2999
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
3000
|
+
const k = 1024;
|
|
3001
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
3002
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${units[i]}`;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
// src/middleware/validation/sanitizers/xss.ts
|
|
3006
|
+
var DEFAULT_ALLOWED_TAGS = [
|
|
3007
|
+
"a",
|
|
3008
|
+
"abbr",
|
|
3009
|
+
"b",
|
|
3010
|
+
"blockquote",
|
|
3011
|
+
"br",
|
|
3012
|
+
"code",
|
|
3013
|
+
"del",
|
|
3014
|
+
"em",
|
|
3015
|
+
"h1",
|
|
3016
|
+
"h2",
|
|
3017
|
+
"h3",
|
|
3018
|
+
"h4",
|
|
3019
|
+
"h5",
|
|
3020
|
+
"h6",
|
|
3021
|
+
"hr",
|
|
3022
|
+
"i",
|
|
3023
|
+
"ins",
|
|
3024
|
+
"li",
|
|
3025
|
+
"mark",
|
|
3026
|
+
"ol",
|
|
3027
|
+
"p",
|
|
3028
|
+
"pre",
|
|
3029
|
+
"q",
|
|
3030
|
+
"s",
|
|
3031
|
+
"small",
|
|
3032
|
+
"span",
|
|
3033
|
+
"strong",
|
|
3034
|
+
"sub",
|
|
3035
|
+
"sup",
|
|
3036
|
+
"u",
|
|
3037
|
+
"ul"
|
|
3038
|
+
];
|
|
3039
|
+
var DEFAULT_ALLOWED_ATTRIBUTES = {
|
|
3040
|
+
a: ["href", "title", "target", "rel"],
|
|
3041
|
+
img: ["src", "alt", "title", "width", "height"],
|
|
3042
|
+
abbr: ["title"],
|
|
3043
|
+
q: ["cite"],
|
|
3044
|
+
blockquote: ["cite"]
|
|
3045
|
+
};
|
|
3046
|
+
var DEFAULT_SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
|
|
3047
|
+
var DANGEROUS_PATTERNS2 = [
|
|
3048
|
+
// Event handlers
|
|
3049
|
+
/\bon\w+\s*=/gi,
|
|
3050
|
+
// JavaScript protocol
|
|
3051
|
+
/javascript\s*:/gi,
|
|
3052
|
+
// VBScript protocol
|
|
3053
|
+
/vbscript\s*:/gi,
|
|
3054
|
+
// Data URI with scripts
|
|
3055
|
+
/data\s*:[^,]*(?:text\/html|application\/javascript|text\/javascript)/gi,
|
|
3056
|
+
// Expression in CSS
|
|
3057
|
+
/expression\s*\(/gi,
|
|
3058
|
+
// Binding in CSS (Firefox)
|
|
3059
|
+
/-moz-binding\s*:/gi,
|
|
3060
|
+
// Behavior in CSS (IE)
|
|
3061
|
+
/behavior\s*:/gi,
|
|
3062
|
+
// Import in CSS
|
|
3063
|
+
/@import/gi,
|
|
3064
|
+
// Script tags
|
|
3065
|
+
/<\s*script/gi,
|
|
3066
|
+
// Style tags with expressions
|
|
3067
|
+
/<\s*style[^>]*>[^<]*expression/gi,
|
|
3068
|
+
// SVG with scripts
|
|
3069
|
+
/<\s*svg[^>]*onload/gi,
|
|
3070
|
+
// Object/embed/applet tags
|
|
3071
|
+
/<\s*(object|embed|applet)/gi,
|
|
3072
|
+
// Base tag (can redirect resources)
|
|
3073
|
+
/<\s*base/gi,
|
|
3074
|
+
// Meta refresh
|
|
3075
|
+
/<\s*meta[^>]*http-equiv\s*=\s*["']?refresh/gi,
|
|
3076
|
+
// Form action hijacking
|
|
3077
|
+
/<\s*form[^>]*action\s*=\s*["']?javascript/gi,
|
|
3078
|
+
// Link tag with import
|
|
3079
|
+
/<\s*link[^>]*rel\s*=\s*["']?import/gi
|
|
3080
|
+
];
|
|
3081
|
+
var HTML_ENTITIES = {
|
|
3082
|
+
"&": "&",
|
|
3083
|
+
"<": "<",
|
|
3084
|
+
">": ">",
|
|
3085
|
+
'"': """,
|
|
3086
|
+
"'": "'",
|
|
3087
|
+
"/": "/",
|
|
3088
|
+
"`": "`",
|
|
3089
|
+
"=": "="
|
|
3090
|
+
};
|
|
3091
|
+
function escapeHtml(str) {
|
|
3092
|
+
return str.replace(/[&<>"'`=/]/g, (char) => HTML_ENTITIES[char] || char);
|
|
3093
|
+
}
|
|
3094
|
+
function unescapeHtml(str) {
|
|
3095
|
+
const entityMap = {
|
|
3096
|
+
"&": "&",
|
|
3097
|
+
"<": "<",
|
|
3098
|
+
">": ">",
|
|
3099
|
+
""": '"',
|
|
3100
|
+
"'": "'",
|
|
3101
|
+
"/": "/",
|
|
3102
|
+
"`": "`",
|
|
3103
|
+
"=": "=",
|
|
3104
|
+
"'": "'",
|
|
3105
|
+
"/": "/"
|
|
3106
|
+
};
|
|
3107
|
+
return str.replace(/&(?:amp|lt|gt|quot|#x27|#x2F|#x60|#x3D|#39|#47);/gi, (entity) => {
|
|
3108
|
+
return entityMap[entity.toLowerCase()] || entity;
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
function stripHtml(str) {
|
|
3112
|
+
let result = str.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
3113
|
+
result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
3114
|
+
result = result.replace(/<[^>]*>/g, "");
|
|
3115
|
+
result = unescapeHtml(result);
|
|
3116
|
+
result = result.replace(/\0/g, "");
|
|
3117
|
+
return result.trim();
|
|
3118
|
+
}
|
|
3119
|
+
function isSafeUrl(url, allowedProtocols = DEFAULT_SAFE_PROTOCOLS) {
|
|
3120
|
+
if (!url) return true;
|
|
3121
|
+
const trimmed = url.trim().toLowerCase();
|
|
3122
|
+
if (trimmed.startsWith("javascript:")) return false;
|
|
3123
|
+
if (trimmed.startsWith("vbscript:")) return false;
|
|
3124
|
+
if (trimmed.startsWith("data:image/")) return true;
|
|
3125
|
+
if (trimmed.startsWith("data:")) return false;
|
|
3126
|
+
try {
|
|
3127
|
+
const parsed = new URL(url, "https://example.com");
|
|
3128
|
+
if (parsed.protocol && !allowedProtocols.includes(parsed.protocol)) {
|
|
3129
|
+
if (!url.includes(":")) return true;
|
|
3130
|
+
return false;
|
|
3131
|
+
}
|
|
3132
|
+
} catch {
|
|
3133
|
+
return true;
|
|
3134
|
+
}
|
|
3135
|
+
return true;
|
|
3136
|
+
}
|
|
3137
|
+
function sanitizeHtml(str, allowedTags = DEFAULT_ALLOWED_TAGS, allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES, allowedProtocols = DEFAULT_SAFE_PROTOCOLS) {
|
|
3138
|
+
let result = str.replace(/\0/g, "");
|
|
3139
|
+
result = result.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
3140
|
+
result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
3141
|
+
result = result.replace(/<!--[\s\S]*?-->/g, "");
|
|
3142
|
+
result = result.replace(/<\/?([a-z][a-z0-9]*)\b([^>]*)>/gi, (match, tagName, attributes) => {
|
|
3143
|
+
const lowerTag = tagName.toLowerCase();
|
|
3144
|
+
const isClosing = match.startsWith("</");
|
|
3145
|
+
if (!allowedTags.includes(lowerTag)) {
|
|
3146
|
+
return "";
|
|
3147
|
+
}
|
|
3148
|
+
if (isClosing) {
|
|
3149
|
+
return `</${lowerTag}>`;
|
|
3150
|
+
}
|
|
3151
|
+
const allowedAttrs = allowedAttributes[lowerTag] || [];
|
|
3152
|
+
const safeAttrs = [];
|
|
3153
|
+
const attrRegex = /([a-z][a-z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*))/gi;
|
|
3154
|
+
let attrMatch;
|
|
3155
|
+
while ((attrMatch = attrRegex.exec(attributes)) !== null) {
|
|
3156
|
+
const attrName = attrMatch[1].toLowerCase();
|
|
3157
|
+
const attrValue = attrMatch[2] || attrMatch[3] || attrMatch[4] || "";
|
|
3158
|
+
if (!allowedAttrs.includes(attrName)) continue;
|
|
3159
|
+
if (DANGEROUS_PATTERNS2.some((pattern) => pattern.test(attrValue))) continue;
|
|
3160
|
+
if (["href", "src", "action", "formaction"].includes(attrName)) {
|
|
3161
|
+
if (!isSafeUrl(attrValue, allowedProtocols)) continue;
|
|
3162
|
+
}
|
|
3163
|
+
const safeValue = escapeHtml(attrValue);
|
|
3164
|
+
safeAttrs.push(`${attrName}="${safeValue}"`);
|
|
3165
|
+
}
|
|
3166
|
+
const attrStr = safeAttrs.length > 0 ? " " + safeAttrs.join(" ") : "";
|
|
3167
|
+
return `<${lowerTag}${attrStr}>`;
|
|
3168
|
+
});
|
|
3169
|
+
for (const pattern of DANGEROUS_PATTERNS2) {
|
|
3170
|
+
result = result.replace(pattern, "");
|
|
3171
|
+
}
|
|
3172
|
+
return result;
|
|
3173
|
+
}
|
|
3174
|
+
function detectXSS(str) {
|
|
3175
|
+
if (!str || typeof str !== "string") return false;
|
|
3176
|
+
const normalized = str.replace(/\\x([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\u([0-9a-f]{4})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);?/gi, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
|
|
3177
|
+
for (const pattern of DANGEROUS_PATTERNS2) {
|
|
3178
|
+
pattern.lastIndex = 0;
|
|
3179
|
+
if (pattern.test(normalized)) {
|
|
3180
|
+
return true;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return false;
|
|
3184
|
+
}
|
|
3185
|
+
function sanitize(input, config = {}) {
|
|
3186
|
+
if (!input || typeof input !== "string") return "";
|
|
3187
|
+
const {
|
|
3188
|
+
mode = "escape",
|
|
3189
|
+
allowedTags = DEFAULT_ALLOWED_TAGS,
|
|
3190
|
+
allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES,
|
|
3191
|
+
allowedProtocols = DEFAULT_SAFE_PROTOCOLS,
|
|
3192
|
+
maxLength,
|
|
3193
|
+
stripNull = true
|
|
3194
|
+
} = config;
|
|
3195
|
+
let result = input;
|
|
3196
|
+
if (stripNull) {
|
|
3197
|
+
result = result.replace(/\0/g, "");
|
|
3198
|
+
}
|
|
3199
|
+
switch (mode) {
|
|
3200
|
+
case "escape":
|
|
3201
|
+
result = escapeHtml(result);
|
|
3202
|
+
break;
|
|
3203
|
+
case "strip":
|
|
3204
|
+
result = stripHtml(result);
|
|
3205
|
+
break;
|
|
3206
|
+
case "allow-safe":
|
|
3207
|
+
result = sanitizeHtml(result, allowedTags, allowedAttributes, allowedProtocols);
|
|
3208
|
+
break;
|
|
3209
|
+
}
|
|
3210
|
+
if (maxLength !== void 0 && result.length > maxLength) {
|
|
3211
|
+
result = result.slice(0, maxLength);
|
|
3212
|
+
}
|
|
3213
|
+
return result;
|
|
3214
|
+
}
|
|
3215
|
+
function sanitizeObject(obj, config = {}) {
|
|
3216
|
+
if (typeof obj === "string") {
|
|
3217
|
+
return sanitize(obj, config);
|
|
3218
|
+
}
|
|
3219
|
+
if (Array.isArray(obj)) {
|
|
3220
|
+
return obj.map((item) => sanitizeObject(item, config));
|
|
3221
|
+
}
|
|
3222
|
+
if (typeof obj === "object" && obj !== null) {
|
|
3223
|
+
const result = {};
|
|
3224
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3225
|
+
result[key] = sanitizeObject(value, config);
|
|
3226
|
+
}
|
|
3227
|
+
return result;
|
|
3228
|
+
}
|
|
3229
|
+
return obj;
|
|
3230
|
+
}
|
|
3231
|
+
function sanitizeFields(obj, fields, config = {}) {
|
|
3232
|
+
const result = { ...obj };
|
|
3233
|
+
for (const field of fields) {
|
|
3234
|
+
if (field in result && typeof result[field] === "string") {
|
|
3235
|
+
result[field] = sanitize(result[field], config);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return result;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// src/middleware/validation/sanitizers/sql.ts
|
|
3242
|
+
var SQL_PATTERNS = [
|
|
3243
|
+
// High severity - Definite attacks
|
|
3244
|
+
{
|
|
3245
|
+
pattern: /'\s*OR\s+'?\d+'?\s*=\s*'?\d+'?/gi,
|
|
3246
|
+
name: "OR '1'='1' attack",
|
|
3247
|
+
severity: "high"
|
|
3248
|
+
},
|
|
3249
|
+
{
|
|
3250
|
+
pattern: /'\s*OR\s+'[^']*'\s*=\s*'[^']*'/gi,
|
|
3251
|
+
name: "OR 'x'='x' attack",
|
|
3252
|
+
severity: "high"
|
|
3253
|
+
},
|
|
3254
|
+
{
|
|
3255
|
+
pattern: /;\s*DROP\s+(TABLE|DATABASE|INDEX|VIEW)/gi,
|
|
3256
|
+
name: "DROP statement",
|
|
3257
|
+
severity: "high"
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
pattern: /;\s*DELETE\s+FROM/gi,
|
|
3261
|
+
name: "DELETE statement",
|
|
3262
|
+
severity: "high"
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
pattern: /;\s*TRUNCATE\s+/gi,
|
|
3266
|
+
name: "TRUNCATE statement",
|
|
3267
|
+
severity: "high"
|
|
3268
|
+
},
|
|
3269
|
+
{
|
|
3270
|
+
pattern: /;\s*INSERT\s+INTO/gi,
|
|
3271
|
+
name: "INSERT statement",
|
|
3272
|
+
severity: "high"
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
pattern: /;\s*UPDATE\s+\w+\s+SET/gi,
|
|
3276
|
+
name: "UPDATE statement",
|
|
3277
|
+
severity: "high"
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
pattern: /UNION\s+(ALL\s+)?SELECT/gi,
|
|
3281
|
+
name: "UNION SELECT attack",
|
|
3282
|
+
severity: "high"
|
|
3283
|
+
},
|
|
3284
|
+
{
|
|
3285
|
+
pattern: /EXEC(\s+|\()+(sp_|xp_)/gi,
|
|
3286
|
+
name: "SQL Server stored procedure",
|
|
3287
|
+
severity: "high"
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
pattern: /EXECUTE\s+IMMEDIATE/gi,
|
|
3291
|
+
name: "Oracle EXECUTE IMMEDIATE",
|
|
3292
|
+
severity: "high"
|
|
3293
|
+
},
|
|
3294
|
+
{
|
|
3295
|
+
pattern: /INTO\s+(OUT|DUMP)FILE/gi,
|
|
3296
|
+
name: "MySQL file write",
|
|
3297
|
+
severity: "high"
|
|
3298
|
+
},
|
|
3299
|
+
{
|
|
3300
|
+
pattern: /LOAD_FILE\s*\(/gi,
|
|
3301
|
+
name: "MySQL file read",
|
|
3302
|
+
severity: "high"
|
|
3303
|
+
},
|
|
3304
|
+
{
|
|
3305
|
+
pattern: /BENCHMARK\s*\(\s*\d+\s*,/gi,
|
|
3306
|
+
name: "MySQL BENCHMARK DoS",
|
|
3307
|
+
severity: "high"
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
pattern: /SLEEP\s*\(\s*\d+\s*\)/gi,
|
|
3311
|
+
name: "SQL SLEEP time-based attack",
|
|
3312
|
+
severity: "high"
|
|
3313
|
+
},
|
|
3314
|
+
{
|
|
3315
|
+
pattern: /WAITFOR\s+DELAY/gi,
|
|
3316
|
+
name: "SQL Server WAITFOR DELAY",
|
|
3317
|
+
severity: "high"
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
pattern: /PG_SLEEP\s*\(/gi,
|
|
3321
|
+
name: "PostgreSQL pg_sleep",
|
|
3322
|
+
severity: "high"
|
|
3323
|
+
},
|
|
3324
|
+
// Medium severity - Likely attacks
|
|
3325
|
+
{
|
|
3326
|
+
pattern: /'\s*--/g,
|
|
3327
|
+
name: "SQL comment injection",
|
|
3328
|
+
severity: "medium"
|
|
3329
|
+
},
|
|
3330
|
+
{
|
|
3331
|
+
pattern: /'\s*#/g,
|
|
3332
|
+
name: "MySQL comment injection",
|
|
3333
|
+
severity: "medium"
|
|
3334
|
+
},
|
|
3335
|
+
{
|
|
3336
|
+
pattern: /\/\*[\s\S]*?\*\//g,
|
|
3337
|
+
name: "Block comment",
|
|
3338
|
+
severity: "medium"
|
|
3339
|
+
},
|
|
3340
|
+
{
|
|
3341
|
+
pattern: /'\s*;\s*$/g,
|
|
3342
|
+
name: "Statement terminator",
|
|
3343
|
+
severity: "medium"
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
pattern: /HAVING\s+\d+\s*=\s*\d+/gi,
|
|
3347
|
+
name: "HAVING clause injection",
|
|
3348
|
+
severity: "medium"
|
|
3349
|
+
},
|
|
3350
|
+
{
|
|
3351
|
+
pattern: /GROUP\s+BY\s+\d+/gi,
|
|
3352
|
+
name: "GROUP BY injection",
|
|
3353
|
+
severity: "medium"
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
pattern: /ORDER\s+BY\s+\d+/gi,
|
|
3357
|
+
name: "ORDER BY injection",
|
|
3358
|
+
severity: "medium"
|
|
3359
|
+
},
|
|
3360
|
+
{
|
|
3361
|
+
pattern: /CONCAT\s*\(/gi,
|
|
3362
|
+
name: "CONCAT function",
|
|
3363
|
+
severity: "medium"
|
|
3364
|
+
},
|
|
3365
|
+
{
|
|
3366
|
+
pattern: /CHAR\s*\(\s*\d+\s*\)/gi,
|
|
3367
|
+
name: "CHAR function bypass",
|
|
3368
|
+
severity: "medium"
|
|
3369
|
+
},
|
|
3370
|
+
{
|
|
3371
|
+
pattern: /0x[0-9a-f]{2,}/gi,
|
|
3372
|
+
name: "Hex encoded value",
|
|
3373
|
+
severity: "medium"
|
|
3374
|
+
},
|
|
3375
|
+
{
|
|
3376
|
+
pattern: /CONVERT\s*\(/gi,
|
|
3377
|
+
name: "CONVERT function",
|
|
3378
|
+
severity: "medium"
|
|
3379
|
+
},
|
|
3380
|
+
{
|
|
3381
|
+
pattern: /CAST\s*\(/gi,
|
|
3382
|
+
name: "CAST function",
|
|
3383
|
+
severity: "medium"
|
|
3384
|
+
},
|
|
3385
|
+
// Low severity - Suspicious but may be false positives
|
|
3386
|
+
{
|
|
3387
|
+
pattern: /'\s*AND\s+'?\d+'?\s*=\s*'?\d+'?/gi,
|
|
3388
|
+
name: "AND '1'='1' pattern",
|
|
3389
|
+
severity: "low"
|
|
3390
|
+
},
|
|
3391
|
+
{
|
|
3392
|
+
pattern: /'\s*AND\s+'[^']*'\s*=\s*'[^']*'/gi,
|
|
3393
|
+
name: "AND 'x'='x' pattern",
|
|
3394
|
+
severity: "low"
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
pattern: /SELECT\s+[\w\s,*]+\s+FROM/gi,
|
|
3398
|
+
name: "SELECT statement",
|
|
3399
|
+
severity: "low"
|
|
3400
|
+
},
|
|
3401
|
+
{
|
|
3402
|
+
pattern: /'\s*\+\s*'/g,
|
|
3403
|
+
name: "String concatenation",
|
|
3404
|
+
severity: "low"
|
|
3405
|
+
},
|
|
3406
|
+
{
|
|
3407
|
+
pattern: /'\s*\|\|\s*'/g,
|
|
3408
|
+
name: "Oracle string concatenation",
|
|
3409
|
+
severity: "low"
|
|
3410
|
+
}
|
|
3411
|
+
];
|
|
3412
|
+
var ENCODED_PATTERNS = [
|
|
3413
|
+
{
|
|
3414
|
+
pattern: /%27\s*%4f%52\s*%27/gi,
|
|
3415
|
+
// URL encoded ' OR '
|
|
3416
|
+
name: "URL encoded OR injection",
|
|
3417
|
+
severity: "high"
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
pattern: /%27\s*%2d%2d/gi,
|
|
3421
|
+
// URL encoded ' --
|
|
3422
|
+
name: "URL encoded comment injection",
|
|
3423
|
+
severity: "medium"
|
|
3424
|
+
},
|
|
3425
|
+
{
|
|
3426
|
+
pattern: /\0|%00/g,
|
|
3427
|
+
// Null byte (decoded or encoded)
|
|
3428
|
+
name: "Null byte injection",
|
|
3429
|
+
severity: "high"
|
|
3430
|
+
},
|
|
3431
|
+
{
|
|
3432
|
+
pattern: /\\x27/gi,
|
|
3433
|
+
// Hex escape
|
|
3434
|
+
name: "Hex escaped quote",
|
|
3435
|
+
severity: "medium"
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
pattern: /\\u0027/gi,
|
|
3439
|
+
// Unicode escape
|
|
3440
|
+
name: "Unicode escaped quote",
|
|
3441
|
+
severity: "medium"
|
|
3442
|
+
}
|
|
3443
|
+
];
|
|
3444
|
+
function normalizeInput(input) {
|
|
3445
|
+
let result = input;
|
|
3446
|
+
try {
|
|
3447
|
+
result = decodeURIComponent(result);
|
|
3448
|
+
} catch {
|
|
3449
|
+
}
|
|
3450
|
+
result = result.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);?/gi, (_, dec) => String.fromCharCode(parseInt(dec, 10))).replace(/"/gi, '"').replace(/'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/&/gi, "&");
|
|
3451
|
+
result = result.replace(
|
|
3452
|
+
/\\x([0-9a-f]{2})/gi,
|
|
3453
|
+
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
3454
|
+
);
|
|
3455
|
+
result = result.replace(
|
|
3456
|
+
/\\u([0-9a-f]{4})/gi,
|
|
3457
|
+
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
3458
|
+
);
|
|
3459
|
+
return result;
|
|
3460
|
+
}
|
|
3461
|
+
function detectSQLInjection(input, options = {}) {
|
|
3462
|
+
if (!input || typeof input !== "string") return [];
|
|
3463
|
+
const {
|
|
3464
|
+
customPatterns = [],
|
|
3465
|
+
checkEncoded = true,
|
|
3466
|
+
minSeverity = "low"
|
|
3467
|
+
} = options;
|
|
3468
|
+
const severityOrder = { low: 0, medium: 1, high: 2 };
|
|
3469
|
+
const minSeverityLevel = severityOrder[minSeverity];
|
|
3470
|
+
const detections = [];
|
|
3471
|
+
const seenPatterns = /* @__PURE__ */ new Set();
|
|
3472
|
+
const normalizedInput = checkEncoded ? normalizeInput(input) : input;
|
|
3473
|
+
const allPatterns = [
|
|
3474
|
+
...SQL_PATTERNS,
|
|
3475
|
+
...checkEncoded ? ENCODED_PATTERNS : [],
|
|
3476
|
+
...customPatterns.map((p) => ({ pattern: p, name: "Custom pattern", severity: "high" }))
|
|
3477
|
+
];
|
|
3478
|
+
for (const { pattern, name, severity } of allPatterns) {
|
|
3479
|
+
if (severityOrder[severity] < minSeverityLevel) continue;
|
|
3480
|
+
pattern.lastIndex = 0;
|
|
3481
|
+
const testInput = checkEncoded ? normalizedInput : input;
|
|
3482
|
+
if (pattern.test(testInput)) {
|
|
3483
|
+
const key = `${name}:${severity}`;
|
|
3484
|
+
if (!seenPatterns.has(key)) {
|
|
3485
|
+
seenPatterns.add(key);
|
|
3486
|
+
detections.push({
|
|
3487
|
+
field: "",
|
|
3488
|
+
// Will be set by caller
|
|
3489
|
+
value: input,
|
|
3490
|
+
pattern: name,
|
|
3491
|
+
severity
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
return detections;
|
|
3497
|
+
}
|
|
3498
|
+
function hasSQLInjection(input, minSeverity = "medium") {
|
|
3499
|
+
return detectSQLInjection(input, { minSeverity }).length > 0;
|
|
3500
|
+
}
|
|
3501
|
+
function sanitizeSQLInput(input) {
|
|
3502
|
+
if (!input || typeof input !== "string") return "";
|
|
3503
|
+
let result = input;
|
|
3504
|
+
result = result.replace(/\0/g, "");
|
|
3505
|
+
result = result.replace(/'/g, "''");
|
|
3506
|
+
result = result.replace(/;/g, "");
|
|
3507
|
+
result = result.replace(/--/g, "");
|
|
3508
|
+
result = result.replace(/\/\*/g, "");
|
|
3509
|
+
result = result.replace(/\*\//g, "");
|
|
3510
|
+
result = result.replace(/0x[0-9a-f]+/gi, "");
|
|
3511
|
+
return result;
|
|
3512
|
+
}
|
|
3513
|
+
function detectSQLInjectionInObject(obj, options = {}) {
|
|
3514
|
+
const { fields, deep = true, customPatterns, minSeverity } = options;
|
|
3515
|
+
const detections = [];
|
|
3516
|
+
function walk(value, path) {
|
|
3517
|
+
if (typeof value === "string") {
|
|
3518
|
+
if (fields && fields.length > 0) {
|
|
3519
|
+
const fieldName = path.split(".").pop() || path;
|
|
3520
|
+
if (!fields.includes(fieldName)) return;
|
|
3521
|
+
}
|
|
3522
|
+
const detected = detectSQLInjection(value, { customPatterns, minSeverity });
|
|
3523
|
+
for (const d of detected) {
|
|
3524
|
+
detections.push({ ...d, field: path });
|
|
3525
|
+
}
|
|
3526
|
+
} else if (deep && Array.isArray(value)) {
|
|
3527
|
+
value.forEach((item, i) => walk(item, `${path}[${i}]`));
|
|
3528
|
+
} else if (deep && typeof value === "object" && value !== null) {
|
|
3529
|
+
for (const [key, val] of Object.entries(value)) {
|
|
3530
|
+
walk(val, path ? `${path}.${key}` : key);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
walk(obj, "");
|
|
3535
|
+
return detections;
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
// src/middleware/validation/middleware.ts
|
|
3539
|
+
function withValidation(handler, config) {
|
|
3540
|
+
const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
|
|
3541
|
+
return async (req) => {
|
|
3542
|
+
const result = await validateRequest(req, {
|
|
3543
|
+
body: config.body,
|
|
3544
|
+
query: config.query,
|
|
3545
|
+
params: config.params,
|
|
3546
|
+
routeParams: config.routeParams
|
|
3547
|
+
});
|
|
3548
|
+
if (!result.success) {
|
|
3549
|
+
return onError(req, result.errors || []);
|
|
3550
|
+
}
|
|
3551
|
+
return handler(req, { validated: result.data });
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
function withSanitization(handler, config = {}) {
|
|
3555
|
+
const {
|
|
3556
|
+
fields,
|
|
3557
|
+
mode = "escape",
|
|
3558
|
+
allowedTags,
|
|
3559
|
+
skip,
|
|
3560
|
+
onSanitized
|
|
3561
|
+
} = config;
|
|
3562
|
+
return async (req) => {
|
|
3563
|
+
if (skip && await skip(req)) {
|
|
3564
|
+
return handler(req, { sanitized: null, changes: [] });
|
|
3565
|
+
}
|
|
3566
|
+
let body;
|
|
3567
|
+
try {
|
|
3568
|
+
body = await req.json();
|
|
3569
|
+
} catch {
|
|
3570
|
+
return handler(req, { sanitized: null, changes: [] });
|
|
3571
|
+
}
|
|
3572
|
+
const changes = [];
|
|
3573
|
+
const sanitized = walkObject(body, (value, path) => {
|
|
3574
|
+
if (fields && fields.length > 0) {
|
|
3575
|
+
const fieldName = path.split(".").pop() || path;
|
|
3576
|
+
if (!fields.includes(fieldName)) {
|
|
3577
|
+
return value;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
const cleaned = sanitize(value, { mode, allowedTags });
|
|
3581
|
+
if (cleaned !== value) {
|
|
3582
|
+
changes.push({
|
|
3583
|
+
field: path,
|
|
3584
|
+
original: value,
|
|
3585
|
+
sanitized: cleaned
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
return cleaned;
|
|
3589
|
+
}, "");
|
|
3590
|
+
if (onSanitized && changes.length > 0) {
|
|
3591
|
+
onSanitized(req, changes);
|
|
3592
|
+
}
|
|
3593
|
+
return handler(req, { sanitized, changes });
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
function withXSSProtection(handler, config = {}) {
|
|
3597
|
+
const { fields, onDetection, checkQuery = true } = config;
|
|
3598
|
+
return async (req) => {
|
|
3599
|
+
const detections = [];
|
|
3600
|
+
if (checkQuery) {
|
|
3601
|
+
const url = new URL(req.url);
|
|
3602
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
3603
|
+
if (detectXSS(value)) {
|
|
3604
|
+
detections.push({ field: `query.${key}`, value });
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
let body;
|
|
3609
|
+
try {
|
|
3610
|
+
body = await req.json();
|
|
3611
|
+
} catch {
|
|
3612
|
+
body = null;
|
|
3613
|
+
}
|
|
3614
|
+
if (body) {
|
|
3615
|
+
walkObject(body, (value, path) => {
|
|
3616
|
+
if (fields && fields.length > 0) {
|
|
3617
|
+
const fieldName = path.split(".").pop() || path;
|
|
3618
|
+
if (!fields.includes(fieldName)) {
|
|
3619
|
+
return value;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
if (detectXSS(value)) {
|
|
3623
|
+
detections.push({ field: path, value });
|
|
3624
|
+
}
|
|
3625
|
+
return value;
|
|
3626
|
+
}, "");
|
|
3627
|
+
}
|
|
3628
|
+
if (detections.length > 0) {
|
|
3629
|
+
if (onDetection) {
|
|
3630
|
+
for (const { field, value } of detections) {
|
|
3631
|
+
const result = await onDetection(req, field, value);
|
|
3632
|
+
if (result instanceof Response) {
|
|
3633
|
+
return result;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return new Response(
|
|
3638
|
+
JSON.stringify({
|
|
3639
|
+
error: "xss_detected",
|
|
3640
|
+
message: "Potentially malicious content detected",
|
|
3641
|
+
fields: detections.map((d) => d.field)
|
|
3642
|
+
}),
|
|
3643
|
+
{
|
|
3644
|
+
status: 400,
|
|
3645
|
+
headers: { "Content-Type": "application/json" }
|
|
3646
|
+
}
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
return handler(req);
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
function withSQLProtection(handler, config = {}) {
|
|
3653
|
+
const {
|
|
3654
|
+
fields,
|
|
3655
|
+
deep = true,
|
|
3656
|
+
mode = "block",
|
|
3657
|
+
customPatterns,
|
|
3658
|
+
allowList = [],
|
|
3659
|
+
onDetection
|
|
3660
|
+
} = config;
|
|
3661
|
+
return async (req) => {
|
|
3662
|
+
let body;
|
|
3663
|
+
try {
|
|
3664
|
+
body = await req.json();
|
|
3665
|
+
} catch {
|
|
3666
|
+
return handler(req);
|
|
3667
|
+
}
|
|
3668
|
+
const detections = detectSQLInjectionInObject(body, {
|
|
3669
|
+
fields,
|
|
3670
|
+
deep,
|
|
3671
|
+
customPatterns,
|
|
3672
|
+
minSeverity: mode === "detect" ? "low" : "medium"
|
|
3673
|
+
});
|
|
3674
|
+
const filtered = detections.filter((d) => !allowList.includes(d.value));
|
|
3675
|
+
if (filtered.length > 0) {
|
|
3676
|
+
if (onDetection) {
|
|
3677
|
+
const result = await onDetection(req, filtered);
|
|
3678
|
+
if (result instanceof Response) {
|
|
3679
|
+
return result;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
if (mode === "block") {
|
|
3683
|
+
return new Response(
|
|
3684
|
+
JSON.stringify({
|
|
3685
|
+
error: "sql_injection_detected",
|
|
3686
|
+
message: "Potentially malicious SQL detected",
|
|
3687
|
+
detections: filtered.map((d) => ({
|
|
3688
|
+
field: d.field,
|
|
3689
|
+
pattern: d.pattern,
|
|
3690
|
+
severity: d.severity
|
|
3691
|
+
}))
|
|
3692
|
+
}),
|
|
3693
|
+
{
|
|
3694
|
+
status: 400,
|
|
3695
|
+
headers: { "Content-Type": "application/json" }
|
|
3696
|
+
}
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return handler(req);
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
function withContentType(handler, config) {
|
|
3704
|
+
const onInvalid = config.onInvalid || ((_, contentType) => defaultContentTypeErrorResponse(contentType, `Content-Type '${contentType}' is not allowed`));
|
|
3705
|
+
return async (req) => {
|
|
3706
|
+
const result = validateContentType(req, config);
|
|
3707
|
+
if (!result.valid) {
|
|
3708
|
+
return onInvalid(req, result.contentType);
|
|
3709
|
+
}
|
|
3710
|
+
return handler(req);
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
function withFileValidation(handler, config = {}) {
|
|
3714
|
+
const onInvalid = config.onInvalid || ((_, errors) => defaultFileErrorResponse(errors));
|
|
3715
|
+
return async (req) => {
|
|
3716
|
+
const result = await validateFilesFromRequest(req, config);
|
|
3717
|
+
if (!result.valid) {
|
|
3718
|
+
return onInvalid(req, result.errors);
|
|
3719
|
+
}
|
|
3720
|
+
return handler(req, { files: result.files });
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
function withSecureValidation(handler, config) {
|
|
3724
|
+
return async (req) => {
|
|
3725
|
+
const allErrors = [];
|
|
3726
|
+
if (config.contentType) {
|
|
3727
|
+
const ctResult = validateContentType(req, config.contentType);
|
|
3728
|
+
if (!ctResult.valid) {
|
|
3729
|
+
allErrors.push({
|
|
3730
|
+
field: "Content-Type",
|
|
3731
|
+
code: "invalid_content_type",
|
|
3732
|
+
message: ctResult.reason || "Invalid Content-Type"
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
let files;
|
|
3737
|
+
if (config.files) {
|
|
3738
|
+
const fileResult = await validateFilesFromRequest(req, config.files);
|
|
3739
|
+
if (!fileResult.valid) {
|
|
3740
|
+
allErrors.push(...fileResult.errors.map((e) => ({
|
|
3741
|
+
field: e.field || e.filename,
|
|
3742
|
+
code: e.code,
|
|
3743
|
+
message: e.message
|
|
3744
|
+
})));
|
|
3745
|
+
} else {
|
|
3746
|
+
files = fileResult.files;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
if (allErrors.length > 0) {
|
|
3750
|
+
const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
|
|
3751
|
+
return onError(req, allErrors);
|
|
3752
|
+
}
|
|
3753
|
+
let validated;
|
|
3754
|
+
if (config.schema) {
|
|
3755
|
+
const schemaResult = await validateRequest(req, {
|
|
3756
|
+
body: config.schema.body,
|
|
3757
|
+
query: config.schema.query,
|
|
3758
|
+
params: config.schema.params,
|
|
3759
|
+
routeParams: config.routeParams
|
|
3760
|
+
});
|
|
3761
|
+
if (!schemaResult.success) {
|
|
3762
|
+
allErrors.push(...schemaResult.errors || []);
|
|
3763
|
+
} else {
|
|
3764
|
+
validated = schemaResult.data;
|
|
3765
|
+
}
|
|
3766
|
+
} else {
|
|
3767
|
+
validated = {
|
|
3768
|
+
body: {},
|
|
3769
|
+
query: {},
|
|
3770
|
+
params: {}
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
if (config.sql && validated?.body) {
|
|
3774
|
+
const sqlDetections = detectSQLInjectionInObject(validated.body, {
|
|
3775
|
+
fields: config.sql.fields,
|
|
3776
|
+
deep: config.sql.deep,
|
|
3777
|
+
customPatterns: config.sql.customPatterns
|
|
3778
|
+
});
|
|
3779
|
+
if (sqlDetections.length > 0 && config.sql.mode !== "detect") {
|
|
3780
|
+
allErrors.push(...sqlDetections.map((d) => ({
|
|
3781
|
+
field: d.field,
|
|
3782
|
+
code: "sql_injection",
|
|
3783
|
+
message: `Potential SQL injection detected: ${d.pattern}`
|
|
3784
|
+
})));
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
if (config.xss?.enabled && validated?.body) {
|
|
3788
|
+
walkObject(validated.body, (value, path) => {
|
|
3789
|
+
if (config.xss?.fields && config.xss.fields.length > 0) {
|
|
3790
|
+
const fieldName = path.split(".").pop() || path;
|
|
3791
|
+
if (!config.xss.fields.includes(fieldName)) {
|
|
3792
|
+
return value;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
if (detectXSS(value)) {
|
|
3796
|
+
allErrors.push({
|
|
3797
|
+
field: path,
|
|
3798
|
+
code: "xss_detected",
|
|
3799
|
+
message: "Potentially malicious content detected"
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
return value;
|
|
3803
|
+
}, "");
|
|
3804
|
+
}
|
|
3805
|
+
if (allErrors.length > 0) {
|
|
3806
|
+
const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
|
|
3807
|
+
return onError(req, allErrors);
|
|
3808
|
+
}
|
|
3809
|
+
return handler(req, { validated, files });
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// src/middleware/audit/stores/memory.ts
|
|
3814
|
+
var MemoryStore2 = class {
|
|
3815
|
+
entries = [];
|
|
3816
|
+
maxEntries;
|
|
3817
|
+
ttl;
|
|
3818
|
+
constructor(options = {}) {
|
|
3819
|
+
this.maxEntries = options.maxEntries || 1e3;
|
|
3820
|
+
this.ttl = options.ttl || 0;
|
|
3821
|
+
}
|
|
3822
|
+
async write(entry) {
|
|
3823
|
+
this.entries.push(entry);
|
|
3824
|
+
if (this.entries.length > this.maxEntries) {
|
|
3825
|
+
this.entries = this.entries.slice(-this.maxEntries);
|
|
3826
|
+
}
|
|
3827
|
+
if (this.ttl > 0) {
|
|
3828
|
+
this.cleanExpired();
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
async query(options = {}) {
|
|
3832
|
+
let result = [...this.entries];
|
|
3833
|
+
if (options.level) {
|
|
3834
|
+
const levels = Array.isArray(options.level) ? options.level : [options.level];
|
|
3835
|
+
result = result.filter((e) => levels.includes(e.level));
|
|
3836
|
+
}
|
|
3837
|
+
if (options.type) {
|
|
3838
|
+
result = result.filter((e) => e.type === options.type);
|
|
3839
|
+
}
|
|
3840
|
+
if (options.event) {
|
|
3841
|
+
const events = Array.isArray(options.event) ? options.event : [options.event];
|
|
3842
|
+
result = result.filter(
|
|
3843
|
+
(e) => e.type === "security" && events.includes(e.event)
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
if (options.startTime) {
|
|
3847
|
+
result = result.filter((e) => e.timestamp >= options.startTime);
|
|
3848
|
+
}
|
|
3849
|
+
if (options.endTime) {
|
|
3850
|
+
result = result.filter((e) => e.timestamp <= options.endTime);
|
|
3851
|
+
}
|
|
3852
|
+
if (options.ip) {
|
|
3853
|
+
result = result.filter((e) => {
|
|
3854
|
+
if (e.type === "request") return e.request.ip === options.ip;
|
|
3855
|
+
if (e.type === "security") return e.source.ip === options.ip;
|
|
3856
|
+
return false;
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
if (options.userId) {
|
|
3860
|
+
result = result.filter((e) => {
|
|
3861
|
+
if (e.type === "request") return e.user?.id === options.userId;
|
|
3862
|
+
if (e.type === "security") return e.source.userId === options.userId;
|
|
3863
|
+
return false;
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
if (options.offset) {
|
|
3867
|
+
result = result.slice(options.offset);
|
|
3868
|
+
}
|
|
3869
|
+
if (options.limit) {
|
|
3870
|
+
result = result.slice(0, options.limit);
|
|
3871
|
+
}
|
|
3872
|
+
return result;
|
|
3873
|
+
}
|
|
3874
|
+
async flush() {
|
|
3875
|
+
}
|
|
3876
|
+
async close() {
|
|
3877
|
+
this.entries = [];
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* Get all entries (for testing)
|
|
3881
|
+
*/
|
|
3882
|
+
getEntries() {
|
|
3883
|
+
return [...this.entries];
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Clear all entries
|
|
3887
|
+
*/
|
|
3888
|
+
clear() {
|
|
3889
|
+
this.entries = [];
|
|
3890
|
+
}
|
|
3891
|
+
/**
|
|
3892
|
+
* Get entry count
|
|
3893
|
+
*/
|
|
3894
|
+
size() {
|
|
3895
|
+
return this.entries.length;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Clean expired entries
|
|
3899
|
+
*/
|
|
3900
|
+
cleanExpired() {
|
|
3901
|
+
if (this.ttl <= 0) return;
|
|
3902
|
+
const now = Date.now();
|
|
3903
|
+
this.entries = this.entries.filter((e) => {
|
|
3904
|
+
const age = now - e.timestamp.getTime();
|
|
3905
|
+
return age < this.ttl;
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
function createMemoryStore2(options) {
|
|
3910
|
+
return new MemoryStore2(options);
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// src/middleware/audit/stores/console.ts
|
|
3914
|
+
var COLORS = {
|
|
3915
|
+
reset: "\x1B[0m",
|
|
3916
|
+
bold: "\x1B[1m",
|
|
3917
|
+
dim: "\x1B[2m",
|
|
3918
|
+
// Log levels
|
|
3919
|
+
debug: "\x1B[36m",
|
|
3920
|
+
// Cyan
|
|
3921
|
+
info: "\x1B[32m",
|
|
3922
|
+
// Green
|
|
3923
|
+
warn: "\x1B[33m",
|
|
3924
|
+
// Yellow
|
|
3925
|
+
error: "\x1B[31m",
|
|
3926
|
+
// Red
|
|
3927
|
+
critical: "\x1B[35m",
|
|
3928
|
+
// Magenta
|
|
3929
|
+
// Security severity
|
|
3930
|
+
low: "\x1B[36m",
|
|
3931
|
+
// Cyan
|
|
3932
|
+
medium: "\x1B[33m",
|
|
3933
|
+
// Yellow
|
|
3934
|
+
high: "\x1B[31m",
|
|
3935
|
+
// Red
|
|
3936
|
+
// Other
|
|
3937
|
+
timestamp: "\x1B[90m",
|
|
3938
|
+
// Gray
|
|
3939
|
+
method: "\x1B[34m",
|
|
3940
|
+
// Blue
|
|
3941
|
+
status2xx: "\x1B[32m",
|
|
3942
|
+
// Green
|
|
3943
|
+
status3xx: "\x1B[36m",
|
|
3944
|
+
// Cyan
|
|
3945
|
+
status4xx: "\x1B[33m",
|
|
3946
|
+
// Yellow
|
|
3947
|
+
status5xx: "\x1B[31m"
|
|
3948
|
+
// Red
|
|
3949
|
+
};
|
|
3950
|
+
var LEVEL_PRIORITY = {
|
|
3951
|
+
debug: 0,
|
|
3952
|
+
info: 1,
|
|
3953
|
+
warn: 2,
|
|
3954
|
+
error: 3,
|
|
3955
|
+
critical: 4
|
|
3956
|
+
};
|
|
3957
|
+
var ConsoleStore = class {
|
|
3958
|
+
colorize;
|
|
3959
|
+
showTimestamp;
|
|
3960
|
+
pretty;
|
|
3961
|
+
minLevel;
|
|
3962
|
+
constructor(options = {}) {
|
|
3963
|
+
this.colorize = options.colorize ?? process.env.NODE_ENV !== "production";
|
|
3964
|
+
this.showTimestamp = options.timestamp ?? true;
|
|
3965
|
+
this.pretty = options.pretty ?? false;
|
|
3966
|
+
this.minLevel = options.level || "info";
|
|
3967
|
+
}
|
|
3968
|
+
async write(entry) {
|
|
3969
|
+
if (LEVEL_PRIORITY[entry.level] < LEVEL_PRIORITY[this.minLevel]) {
|
|
3970
|
+
return;
|
|
3971
|
+
}
|
|
3972
|
+
const output = this.pretty ? this.formatPretty(entry) : this.formatCompact(entry);
|
|
3973
|
+
switch (entry.level) {
|
|
3974
|
+
case "debug":
|
|
3975
|
+
console.debug(output);
|
|
3976
|
+
break;
|
|
3977
|
+
case "info":
|
|
3978
|
+
console.info(output);
|
|
3979
|
+
break;
|
|
3980
|
+
case "warn":
|
|
3981
|
+
console.warn(output);
|
|
3982
|
+
break;
|
|
3983
|
+
case "error":
|
|
3984
|
+
case "critical":
|
|
3985
|
+
console.error(output);
|
|
3986
|
+
break;
|
|
3987
|
+
default:
|
|
3988
|
+
console.log(output);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
async flush() {
|
|
3992
|
+
}
|
|
3993
|
+
async close() {
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* Format entry in compact single-line format
|
|
3997
|
+
*/
|
|
3998
|
+
formatCompact(entry) {
|
|
3999
|
+
const parts = [];
|
|
4000
|
+
if (this.showTimestamp) {
|
|
4001
|
+
const ts = entry.timestamp.toISOString();
|
|
4002
|
+
parts.push(this.color(ts, "timestamp"));
|
|
4003
|
+
}
|
|
4004
|
+
parts.push(this.colorLevel(entry.level));
|
|
4005
|
+
if (entry.type === "request") {
|
|
4006
|
+
const req = entry.request;
|
|
4007
|
+
const res = entry.response;
|
|
4008
|
+
parts.push(this.color(req.method, "method"));
|
|
4009
|
+
parts.push(req.path);
|
|
4010
|
+
if (res) {
|
|
4011
|
+
parts.push(this.colorStatus(res.status));
|
|
4012
|
+
parts.push(this.color(`${res.duration}ms`, "dim"));
|
|
4013
|
+
}
|
|
4014
|
+
if (req.ip) {
|
|
4015
|
+
parts.push(this.color(`[${req.ip}]`, "dim"));
|
|
4016
|
+
}
|
|
4017
|
+
if (entry.user?.id) {
|
|
4018
|
+
parts.push(this.color(`user:${entry.user.id}`, "dim"));
|
|
4019
|
+
}
|
|
4020
|
+
if (entry.error) {
|
|
4021
|
+
parts.push(this.color(`ERROR: ${entry.error.message}`, "error"));
|
|
4022
|
+
}
|
|
4023
|
+
} else if (entry.type === "security") {
|
|
4024
|
+
parts.push(this.colorSeverity(entry.severity));
|
|
4025
|
+
parts.push(entry.event);
|
|
4026
|
+
if (entry.source.ip) {
|
|
4027
|
+
parts.push(this.color(`[${entry.source.ip}]`, "dim"));
|
|
4028
|
+
}
|
|
4029
|
+
if (entry.source.userId) {
|
|
4030
|
+
parts.push(this.color(`user:${entry.source.userId}`, "dim"));
|
|
4031
|
+
}
|
|
4032
|
+
parts.push(entry.message);
|
|
4033
|
+
}
|
|
4034
|
+
return parts.join(" ");
|
|
4035
|
+
}
|
|
4036
|
+
/**
|
|
4037
|
+
* Format entry in pretty multi-line format
|
|
4038
|
+
*/
|
|
4039
|
+
formatPretty(entry) {
|
|
4040
|
+
const lines = [];
|
|
4041
|
+
const header = [
|
|
4042
|
+
this.color(entry.timestamp.toISOString(), "timestamp"),
|
|
4043
|
+
this.colorLevel(entry.level),
|
|
4044
|
+
`[${entry.type.toUpperCase()}]`
|
|
4045
|
+
].join(" ");
|
|
4046
|
+
lines.push(header);
|
|
4047
|
+
if (entry.type === "request") {
|
|
4048
|
+
const req = entry.request;
|
|
4049
|
+
const res = entry.response;
|
|
4050
|
+
lines.push(` ${this.color(req.method, "method")} ${req.url}`);
|
|
4051
|
+
if (req.ip) lines.push(` IP: ${req.ip}`);
|
|
4052
|
+
if (req.userAgent) lines.push(` UA: ${req.userAgent}`);
|
|
4053
|
+
if (res) {
|
|
4054
|
+
lines.push(` Status: ${this.colorStatus(res.status)} (${res.duration}ms)`);
|
|
4055
|
+
}
|
|
4056
|
+
if (entry.user) {
|
|
4057
|
+
lines.push(` User: ${JSON.stringify(entry.user)}`);
|
|
4058
|
+
}
|
|
4059
|
+
if (entry.error) {
|
|
4060
|
+
lines.push(` ${this.color("Error:", "error")} ${entry.error.message}`);
|
|
4061
|
+
if (entry.error.stack) {
|
|
4062
|
+
lines.push(` ${this.color(entry.error.stack, "dim")}`);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
} else if (entry.type === "security") {
|
|
4066
|
+
lines.push(` Event: ${entry.event}`);
|
|
4067
|
+
lines.push(` Severity: ${this.colorSeverity(entry.severity)}`);
|
|
4068
|
+
lines.push(` Message: ${entry.message}`);
|
|
4069
|
+
if (entry.source.ip) lines.push(` Source IP: ${entry.source.ip}`);
|
|
4070
|
+
if (entry.source.userId) lines.push(` Source User: ${entry.source.userId}`);
|
|
4071
|
+
if (entry.target) {
|
|
4072
|
+
lines.push(` Target: ${JSON.stringify(entry.target)}`);
|
|
4073
|
+
}
|
|
4074
|
+
if (entry.details) {
|
|
4075
|
+
lines.push(` Details: ${JSON.stringify(entry.details)}`);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
|
|
4079
|
+
lines.push(` Metadata: ${JSON.stringify(entry.metadata)}`);
|
|
4080
|
+
}
|
|
4081
|
+
return lines.join("\n");
|
|
4082
|
+
}
|
|
4083
|
+
/**
|
|
4084
|
+
* Apply color if enabled
|
|
4085
|
+
*/
|
|
4086
|
+
color(text, colorName) {
|
|
4087
|
+
if (!this.colorize) return text;
|
|
4088
|
+
return `${COLORS[colorName]}${text}${COLORS.reset}`;
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Color log level
|
|
4092
|
+
*/
|
|
4093
|
+
colorLevel(level) {
|
|
4094
|
+
const text = level.toUpperCase().padEnd(8);
|
|
4095
|
+
if (!this.colorize) return `[${text}]`;
|
|
4096
|
+
return `[${COLORS[level]}${text}${COLORS.reset}]`;
|
|
4097
|
+
}
|
|
4098
|
+
/**
|
|
4099
|
+
* Color HTTP status
|
|
4100
|
+
*/
|
|
4101
|
+
colorStatus(status) {
|
|
4102
|
+
const text = status.toString();
|
|
4103
|
+
if (!this.colorize) return text;
|
|
4104
|
+
if (status >= 500) return `${COLORS.status5xx}${text}${COLORS.reset}`;
|
|
4105
|
+
if (status >= 400) return `${COLORS.status4xx}${text}${COLORS.reset}`;
|
|
4106
|
+
if (status >= 300) return `${COLORS.status3xx}${text}${COLORS.reset}`;
|
|
4107
|
+
return `${COLORS.status2xx}${text}${COLORS.reset}`;
|
|
4108
|
+
}
|
|
4109
|
+
/**
|
|
4110
|
+
* Color severity
|
|
4111
|
+
*/
|
|
4112
|
+
colorSeverity(severity) {
|
|
4113
|
+
const text = `[${severity.toUpperCase()}]`;
|
|
4114
|
+
if (!this.colorize) return text;
|
|
4115
|
+
const colorKey = severity === "critical" ? "critical" : severity;
|
|
4116
|
+
return `${COLORS[colorKey]}${text}${COLORS.reset}`;
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
function createConsoleStore(options) {
|
|
4120
|
+
return new ConsoleStore(options);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
// src/middleware/audit/stores/external.ts
|
|
4124
|
+
var ExternalStore = class {
|
|
4125
|
+
endpoint;
|
|
4126
|
+
headers;
|
|
4127
|
+
batchSize;
|
|
4128
|
+
flushInterval;
|
|
4129
|
+
retryAttempts;
|
|
4130
|
+
timeout;
|
|
4131
|
+
buffer = [];
|
|
4132
|
+
flushTimer = null;
|
|
4133
|
+
isFlushing = false;
|
|
4134
|
+
constructor(options) {
|
|
4135
|
+
this.endpoint = options.endpoint;
|
|
4136
|
+
this.headers = {
|
|
4137
|
+
"Content-Type": "application/json",
|
|
4138
|
+
...options.apiKey ? { "Authorization": `Bearer ${options.apiKey}` } : {},
|
|
4139
|
+
...options.headers
|
|
4140
|
+
};
|
|
4141
|
+
this.batchSize = options.batchSize || 100;
|
|
4142
|
+
this.flushInterval = options.flushInterval || 5e3;
|
|
4143
|
+
this.retryAttempts = options.retryAttempts || 3;
|
|
4144
|
+
this.timeout = options.timeout || 1e4;
|
|
4145
|
+
if (this.flushInterval > 0) {
|
|
4146
|
+
this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
async write(entry) {
|
|
4150
|
+
this.buffer.push(entry);
|
|
4151
|
+
if (this.buffer.length >= this.batchSize) {
|
|
4152
|
+
await this.flush();
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
async flush() {
|
|
4156
|
+
if (this.isFlushing || this.buffer.length === 0) return;
|
|
4157
|
+
this.isFlushing = true;
|
|
4158
|
+
const entries = [...this.buffer];
|
|
4159
|
+
this.buffer = [];
|
|
4160
|
+
try {
|
|
4161
|
+
await this.send(entries);
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
this.buffer = [...entries, ...this.buffer];
|
|
4164
|
+
console.error("[ExternalStore] Failed to flush logs:", error);
|
|
4165
|
+
} finally {
|
|
4166
|
+
this.isFlushing = false;
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
async close() {
|
|
4170
|
+
if (this.flushTimer) {
|
|
4171
|
+
clearInterval(this.flushTimer);
|
|
4172
|
+
this.flushTimer = null;
|
|
4173
|
+
}
|
|
4174
|
+
await this.flush();
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Send entries to external endpoint
|
|
4178
|
+
*/
|
|
4179
|
+
async send(entries) {
|
|
4180
|
+
let lastError = null;
|
|
4181
|
+
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
|
|
4182
|
+
try {
|
|
4183
|
+
const controller = new AbortController();
|
|
4184
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
4185
|
+
const response = await fetch(this.endpoint, {
|
|
4186
|
+
method: "POST",
|
|
4187
|
+
headers: this.headers,
|
|
4188
|
+
body: JSON.stringify({
|
|
4189
|
+
logs: entries.map((e) => this.serialize(e)),
|
|
4190
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4191
|
+
count: entries.length
|
|
4192
|
+
}),
|
|
4193
|
+
signal: controller.signal
|
|
4194
|
+
});
|
|
4195
|
+
clearTimeout(timeoutId);
|
|
4196
|
+
if (!response.ok) {
|
|
4197
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4198
|
+
}
|
|
4199
|
+
return;
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
4202
|
+
if (attempt < this.retryAttempts - 1) {
|
|
4203
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
throw lastError || new Error("Failed to send logs");
|
|
4208
|
+
}
|
|
4209
|
+
/**
|
|
4210
|
+
* Serialize entry for transmission
|
|
4211
|
+
*/
|
|
4212
|
+
serialize(entry) {
|
|
4213
|
+
return {
|
|
4214
|
+
...entry,
|
|
4215
|
+
timestamp: entry.timestamp.toISOString()
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Sleep helper
|
|
4220
|
+
*/
|
|
4221
|
+
sleep(ms) {
|
|
4222
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4223
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Get buffer size (for monitoring)
|
|
4226
|
+
*/
|
|
4227
|
+
getBufferSize() {
|
|
4228
|
+
return this.buffer.length;
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4231
|
+
function createExternalStore(options) {
|
|
4232
|
+
return new ExternalStore(options);
|
|
4233
|
+
}
|
|
4234
|
+
function createDatadogStore(options) {
|
|
4235
|
+
const site = options.site || "datadoghq.com";
|
|
4236
|
+
const endpoint = `https://http-intake.logs.${site}/api/v2/logs`;
|
|
4237
|
+
return new ExternalStore({
|
|
4238
|
+
endpoint,
|
|
4239
|
+
headers: {
|
|
4240
|
+
"DD-API-KEY": options.apiKey,
|
|
4241
|
+
"Content-Type": "application/json"
|
|
4242
|
+
},
|
|
4243
|
+
batchSize: options.batchSize || 100,
|
|
4244
|
+
flushInterval: options.flushInterval || 5e3
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
var MultiStore = class {
|
|
4248
|
+
stores;
|
|
4249
|
+
constructor(stores) {
|
|
4250
|
+
this.stores = stores;
|
|
4251
|
+
}
|
|
4252
|
+
async write(entry) {
|
|
4253
|
+
await Promise.all(this.stores.map((store) => store.write(entry)));
|
|
4254
|
+
}
|
|
4255
|
+
async query(options) {
|
|
4256
|
+
for (const store of this.stores) {
|
|
4257
|
+
if (store.query) {
|
|
4258
|
+
return store.query(options);
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
return [];
|
|
4262
|
+
}
|
|
4263
|
+
async flush() {
|
|
4264
|
+
await Promise.all(this.stores.map((store) => store.flush?.()));
|
|
4265
|
+
}
|
|
4266
|
+
async close() {
|
|
4267
|
+
await Promise.all(this.stores.map((store) => store.close?.()));
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
function createMultiStore(stores) {
|
|
4271
|
+
return new MultiStore(stores);
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
// src/middleware/audit/formatters.ts
|
|
4275
|
+
var JSONFormatter = class {
|
|
4276
|
+
pretty;
|
|
4277
|
+
includeTimestamp;
|
|
4278
|
+
constructor(options = {}) {
|
|
4279
|
+
this.pretty = options.pretty ?? false;
|
|
4280
|
+
this.includeTimestamp = options.includeTimestamp ?? true;
|
|
4281
|
+
}
|
|
4282
|
+
format(entry) {
|
|
4283
|
+
const output = {
|
|
4284
|
+
...entry,
|
|
4285
|
+
timestamp: this.includeTimestamp ? entry.timestamp.toISOString() : void 0
|
|
4286
|
+
};
|
|
4287
|
+
return this.pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
|
|
4288
|
+
}
|
|
4289
|
+
};
|
|
4290
|
+
var TextFormatter = class {
|
|
4291
|
+
template;
|
|
4292
|
+
dateFormat;
|
|
4293
|
+
constructor(options = {}) {
|
|
4294
|
+
this.template = options.template || "{timestamp} [{level}] {message}";
|
|
4295
|
+
this.dateFormat = options.dateFormat || "iso";
|
|
4296
|
+
}
|
|
4297
|
+
format(entry) {
|
|
4298
|
+
let output = this.template;
|
|
4299
|
+
output = output.replace("{timestamp}", this.formatDate(entry.timestamp));
|
|
4300
|
+
output = output.replace("{level}", entry.level.toUpperCase().padEnd(8));
|
|
4301
|
+
output = output.replace("{message}", entry.message);
|
|
4302
|
+
output = output.replace("{type}", entry.type);
|
|
4303
|
+
output = output.replace("{id}", entry.id);
|
|
4304
|
+
if (entry.category) {
|
|
4305
|
+
output = output.replace("{category}", entry.category);
|
|
4306
|
+
}
|
|
4307
|
+
if (entry.type === "request") {
|
|
4308
|
+
output = output.replace("{method}", entry.request.method);
|
|
4309
|
+
output = output.replace("{path}", entry.request.path);
|
|
4310
|
+
output = output.replace("{url}", entry.request.url);
|
|
4311
|
+
output = output.replace("{ip}", entry.request.ip || "-");
|
|
4312
|
+
output = output.replace("{status}", entry.response?.status?.toString() || "-");
|
|
4313
|
+
output = output.replace("{duration}", entry.response?.duration?.toString() || "-");
|
|
4314
|
+
}
|
|
4315
|
+
if (entry.type === "security") {
|
|
4316
|
+
output = output.replace("{event}", entry.event);
|
|
4317
|
+
output = output.replace("{severity}", entry.severity);
|
|
4318
|
+
}
|
|
4319
|
+
return output;
|
|
4320
|
+
}
|
|
4321
|
+
formatDate(date) {
|
|
4322
|
+
switch (this.dateFormat) {
|
|
4323
|
+
case "utc":
|
|
4324
|
+
return date.toUTCString();
|
|
4325
|
+
case "local":
|
|
4326
|
+
return date.toLocaleString();
|
|
4327
|
+
case "iso":
|
|
4328
|
+
default:
|
|
4329
|
+
return date.toISOString();
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
};
|
|
4333
|
+
var CLFFormatter = class {
|
|
4334
|
+
format(entry) {
|
|
4335
|
+
if (entry.type !== "request") {
|
|
4336
|
+
return `[${entry.timestamp.toISOString()}] ${entry.level.toUpperCase()} ${entry.message}`;
|
|
4337
|
+
}
|
|
4338
|
+
const req = entry.request;
|
|
4339
|
+
const res = entry.response;
|
|
4340
|
+
const host = req.ip || "-";
|
|
4341
|
+
const ident = "-";
|
|
4342
|
+
const authuser = entry.user?.id || "-";
|
|
4343
|
+
const date = this.formatCLFDate(entry.timestamp);
|
|
4344
|
+
const request = `${req.method} ${req.path} HTTP/1.1`;
|
|
4345
|
+
const status = res?.status || 0;
|
|
4346
|
+
const bytes = res?.contentLength || 0;
|
|
4347
|
+
return `${host} ${ident} ${authuser} [${date}] "${request}" ${status} ${bytes}`;
|
|
4348
|
+
}
|
|
4349
|
+
formatCLFDate(date) {
|
|
4350
|
+
const months = [
|
|
4351
|
+
"Jan",
|
|
4352
|
+
"Feb",
|
|
4353
|
+
"Mar",
|
|
4354
|
+
"Apr",
|
|
4355
|
+
"May",
|
|
4356
|
+
"Jun",
|
|
4357
|
+
"Jul",
|
|
4358
|
+
"Aug",
|
|
4359
|
+
"Sep",
|
|
4360
|
+
"Oct",
|
|
4361
|
+
"Nov",
|
|
4362
|
+
"Dec"
|
|
4363
|
+
];
|
|
4364
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
4365
|
+
const month = months[date.getMonth()];
|
|
4366
|
+
const year = date.getFullYear();
|
|
4367
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
4368
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
4369
|
+
const seconds = date.getSeconds().toString().padStart(2, "0");
|
|
4370
|
+
const offset = -date.getTimezoneOffset();
|
|
4371
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
4372
|
+
const offsetHours = Math.floor(Math.abs(offset) / 60).toString().padStart(2, "0");
|
|
4373
|
+
const offsetMins = (Math.abs(offset) % 60).toString().padStart(2, "0");
|
|
4374
|
+
return `${day}/${month}/${year}:${hours}:${minutes}:${seconds} ${offsetSign}${offsetHours}${offsetMins}`;
|
|
4375
|
+
}
|
|
4376
|
+
};
|
|
4377
|
+
var StructuredFormatter = class {
|
|
4378
|
+
delimiter;
|
|
4379
|
+
kvSeparator;
|
|
4380
|
+
constructor(options = {}) {
|
|
4381
|
+
this.delimiter = options.delimiter || " ";
|
|
4382
|
+
this.kvSeparator = options.kvSeparator || "=";
|
|
4383
|
+
}
|
|
4384
|
+
format(entry) {
|
|
4385
|
+
const pairs = [];
|
|
4386
|
+
pairs.push(this.pair("timestamp", entry.timestamp.toISOString()));
|
|
4387
|
+
pairs.push(this.pair("level", entry.level));
|
|
4388
|
+
pairs.push(this.pair("type", entry.type));
|
|
4389
|
+
pairs.push(this.pair("id", entry.id));
|
|
4390
|
+
pairs.push(this.pair("message", this.escape(entry.message)));
|
|
4391
|
+
if (entry.category) {
|
|
4392
|
+
pairs.push(this.pair("category", entry.category));
|
|
4393
|
+
}
|
|
4394
|
+
if (entry.type === "request") {
|
|
4395
|
+
pairs.push(this.pair("method", entry.request.method));
|
|
4396
|
+
pairs.push(this.pair("path", entry.request.path));
|
|
4397
|
+
if (entry.request.ip) pairs.push(this.pair("ip", entry.request.ip));
|
|
4398
|
+
if (entry.response) {
|
|
4399
|
+
pairs.push(this.pair("status", entry.response.status.toString()));
|
|
4400
|
+
pairs.push(this.pair("duration_ms", entry.response.duration.toString()));
|
|
4401
|
+
}
|
|
4402
|
+
if (entry.user?.id) pairs.push(this.pair("user_id", entry.user.id));
|
|
4403
|
+
if (entry.error) {
|
|
4404
|
+
pairs.push(this.pair("error", this.escape(entry.error.message)));
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
if (entry.type === "security") {
|
|
4408
|
+
pairs.push(this.pair("event", entry.event));
|
|
4409
|
+
pairs.push(this.pair("severity", entry.severity));
|
|
4410
|
+
if (entry.source.ip) pairs.push(this.pair("source_ip", entry.source.ip));
|
|
4411
|
+
if (entry.source.userId) pairs.push(this.pair("source_user", entry.source.userId));
|
|
4412
|
+
}
|
|
4413
|
+
return pairs.join(this.delimiter);
|
|
4414
|
+
}
|
|
4415
|
+
pair(key, value) {
|
|
4416
|
+
return `${key}${this.kvSeparator}${value}`;
|
|
4417
|
+
}
|
|
4418
|
+
escape(value) {
|
|
4419
|
+
if (value.includes(" ") || value.includes('"')) {
|
|
4420
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
4421
|
+
}
|
|
4422
|
+
return value;
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4425
|
+
function createJSONFormatter(options) {
|
|
4426
|
+
return new JSONFormatter(options);
|
|
4427
|
+
}
|
|
4428
|
+
function createTextFormatter(options) {
|
|
4429
|
+
return new TextFormatter(options);
|
|
4430
|
+
}
|
|
4431
|
+
function createCLFFormatter() {
|
|
4432
|
+
return new CLFFormatter();
|
|
4433
|
+
}
|
|
4434
|
+
function createStructuredFormatter(options) {
|
|
4435
|
+
return new StructuredFormatter(options);
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
// src/middleware/audit/redaction.ts
|
|
4439
|
+
var DEFAULT_PII_FIELDS = [
|
|
4440
|
+
// Authentication
|
|
4441
|
+
"password",
|
|
4442
|
+
"passwd",
|
|
4443
|
+
"secret",
|
|
4444
|
+
"token",
|
|
4445
|
+
"api_key",
|
|
4446
|
+
"apiKey",
|
|
4447
|
+
"api-key",
|
|
4448
|
+
"access_token",
|
|
4449
|
+
"accessToken",
|
|
4450
|
+
"refresh_token",
|
|
4451
|
+
"refreshToken",
|
|
4452
|
+
"authorization",
|
|
4453
|
+
"auth",
|
|
4454
|
+
// Personal information
|
|
4455
|
+
"ssn",
|
|
4456
|
+
"social_security",
|
|
4457
|
+
"socialSecurity",
|
|
4458
|
+
"credit_card",
|
|
4459
|
+
"creditCard",
|
|
4460
|
+
"card_number",
|
|
4461
|
+
"cardNumber",
|
|
4462
|
+
"cvv",
|
|
4463
|
+
"cvc",
|
|
4464
|
+
"pin",
|
|
4465
|
+
// Contact
|
|
4466
|
+
"email",
|
|
4467
|
+
"phone",
|
|
4468
|
+
"phone_number",
|
|
4469
|
+
"phoneNumber",
|
|
4470
|
+
"mobile",
|
|
4471
|
+
"address",
|
|
4472
|
+
"street",
|
|
4473
|
+
"zip",
|
|
4474
|
+
"zipcode",
|
|
4475
|
+
"postal_code",
|
|
4476
|
+
"postalCode",
|
|
4477
|
+
// Identity
|
|
4478
|
+
"date_of_birth",
|
|
4479
|
+
"dateOfBirth",
|
|
4480
|
+
"dob",
|
|
4481
|
+
"birth_date",
|
|
4482
|
+
"birthDate",
|
|
4483
|
+
"passport",
|
|
4484
|
+
"license",
|
|
4485
|
+
"national_id",
|
|
4486
|
+
"nationalId"
|
|
4487
|
+
];
|
|
4488
|
+
function mask(value, options = {}) {
|
|
4489
|
+
const {
|
|
4490
|
+
char = "*",
|
|
4491
|
+
preserveLength = false,
|
|
4492
|
+
showFirst = 0,
|
|
4493
|
+
showLast = 0
|
|
4494
|
+
} = options;
|
|
4495
|
+
if (!value) return value;
|
|
4496
|
+
const len = value.length;
|
|
4497
|
+
if (preserveLength) {
|
|
4498
|
+
const first2 = value.slice(0, showFirst);
|
|
4499
|
+
const last2 = value.slice(-showLast || len);
|
|
4500
|
+
const middle = char.repeat(Math.max(0, len - showFirst - showLast));
|
|
4501
|
+
return first2 + middle + (showLast > 0 ? last2 : "");
|
|
4502
|
+
}
|
|
4503
|
+
const maskLen = 8;
|
|
4504
|
+
const first = showFirst > 0 ? value.slice(0, showFirst) : "";
|
|
4505
|
+
const last = showLast > 0 ? value.slice(-showLast) : "";
|
|
4506
|
+
return first + char.repeat(maskLen) + last;
|
|
4507
|
+
}
|
|
4508
|
+
function hash(value, salt = "") {
|
|
4509
|
+
const str = salt + value;
|
|
4510
|
+
let hash2 = 0;
|
|
4511
|
+
for (let i = 0; i < str.length; i++) {
|
|
4512
|
+
const char = str.charCodeAt(i);
|
|
4513
|
+
hash2 = (hash2 << 5) - hash2 + char;
|
|
4514
|
+
hash2 = hash2 & hash2;
|
|
4515
|
+
}
|
|
4516
|
+
const hex = Math.abs(hash2).toString(16).padStart(8, "0");
|
|
4517
|
+
return hex + hex.slice(0, 8);
|
|
4518
|
+
}
|
|
4519
|
+
function redactValue(value, field, config) {
|
|
4520
|
+
if (typeof value !== "string") return value;
|
|
4521
|
+
if (!value) return value;
|
|
4522
|
+
const shouldRedact = config.fields.some((f) => {
|
|
4523
|
+
const fieldLower = field.toLowerCase();
|
|
4524
|
+
const fLower = f.toLowerCase();
|
|
4525
|
+
return fieldLower === fLower || fieldLower.endsWith("." + fLower) || fieldLower.includes("[" + fLower + "]");
|
|
4526
|
+
});
|
|
4527
|
+
if (!shouldRedact) return value;
|
|
4528
|
+
if (config.customRedactor) {
|
|
4529
|
+
return config.customRedactor(value, field);
|
|
4530
|
+
}
|
|
4531
|
+
switch (config.mode) {
|
|
4532
|
+
case "mask":
|
|
4533
|
+
return mask(value, {
|
|
4534
|
+
char: config.maskChar || "*",
|
|
4535
|
+
preserveLength: config.preserveLength,
|
|
4536
|
+
showFirst: 2,
|
|
4537
|
+
showLast: 2
|
|
4538
|
+
});
|
|
4539
|
+
case "hash":
|
|
4540
|
+
return `[HASH:${hash(value)}]`;
|
|
4541
|
+
case "remove":
|
|
4542
|
+
return "[REDACTED]";
|
|
4543
|
+
default:
|
|
4544
|
+
return "[REDACTED]";
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
function redactObject(obj, config, path = "") {
|
|
4548
|
+
if (typeof obj === "string") {
|
|
4549
|
+
return redactValue(obj, path, config);
|
|
4550
|
+
}
|
|
4551
|
+
if (Array.isArray(obj)) {
|
|
4552
|
+
return obj.map((item, i) => redactObject(item, config, `${path}[${i}]`));
|
|
4553
|
+
}
|
|
4554
|
+
if (typeof obj === "object" && obj !== null) {
|
|
4555
|
+
const result = {};
|
|
4556
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
4557
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
4558
|
+
result[key] = redactObject(value, config, newPath);
|
|
4559
|
+
}
|
|
4560
|
+
return result;
|
|
4561
|
+
}
|
|
4562
|
+
return obj;
|
|
4563
|
+
}
|
|
4564
|
+
function createRedactor(config = {}) {
|
|
4565
|
+
const fullConfig = {
|
|
4566
|
+
fields: config.fields || DEFAULT_PII_FIELDS,
|
|
4567
|
+
mode: config.mode || "mask",
|
|
4568
|
+
maskChar: config.maskChar || "*",
|
|
4569
|
+
preserveLength: config.preserveLength || false,
|
|
4570
|
+
customRedactor: config.customRedactor
|
|
4571
|
+
};
|
|
4572
|
+
return (obj) => redactObject(obj, fullConfig);
|
|
4573
|
+
}
|
|
4574
|
+
function redactHeaders(headers, sensitiveHeaders = ["authorization", "cookie", "x-api-key", "x-auth-token"]) {
|
|
4575
|
+
const result = {};
|
|
4576
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4577
|
+
const keyLower = key.toLowerCase();
|
|
4578
|
+
if (sensitiveHeaders.some((h) => keyLower === h.toLowerCase())) {
|
|
4579
|
+
result[key] = "[REDACTED]";
|
|
4580
|
+
} else {
|
|
4581
|
+
result[key] = value;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
return result;
|
|
4585
|
+
}
|
|
4586
|
+
function redactQuery(query, sensitiveParams = ["token", "key", "secret", "password", "auth"]) {
|
|
4587
|
+
const result = {};
|
|
4588
|
+
for (const [key, value] of Object.entries(query)) {
|
|
4589
|
+
const keyLower = key.toLowerCase();
|
|
4590
|
+
if (sensitiveParams.some((p) => keyLower.includes(p.toLowerCase()))) {
|
|
4591
|
+
result[key] = "[REDACTED]";
|
|
4592
|
+
} else {
|
|
4593
|
+
result[key] = value;
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
return result;
|
|
4597
|
+
}
|
|
4598
|
+
function redactEmail(email) {
|
|
4599
|
+
if (!email || !email.includes("@")) return mask(email);
|
|
4600
|
+
const [, domain] = email.split("@");
|
|
4601
|
+
return `****@${domain}`;
|
|
4602
|
+
}
|
|
4603
|
+
function redactCreditCard(cardNumber) {
|
|
4604
|
+
const cleaned = cardNumber.replace(/\D/g, "");
|
|
4605
|
+
if (cleaned.length < 4) return mask(cardNumber);
|
|
4606
|
+
return "**** **** **** " + cleaned.slice(-4);
|
|
4607
|
+
}
|
|
4608
|
+
function redactPhone(phone) {
|
|
4609
|
+
const cleaned = phone.replace(/\D/g, "");
|
|
4610
|
+
if (cleaned.length < 4) return mask(phone);
|
|
4611
|
+
return mask(phone, { preserveLength: true, showLast: 4 });
|
|
4612
|
+
}
|
|
4613
|
+
function redactIP(ip) {
|
|
4614
|
+
if (ip.includes(":")) {
|
|
4615
|
+
const parts2 = ip.split(":");
|
|
4616
|
+
return parts2[0] + ":****:****:****";
|
|
4617
|
+
}
|
|
4618
|
+
const parts = ip.split(".");
|
|
4619
|
+
if (parts.length !== 4) return mask(ip);
|
|
4620
|
+
return `${parts[0]}.${parts[1]}.*.*`;
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
// src/middleware/audit/events.ts
|
|
4624
|
+
function generateId() {
|
|
4625
|
+
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
4626
|
+
}
|
|
4627
|
+
function severityToLevel(severity) {
|
|
4628
|
+
switch (severity) {
|
|
4629
|
+
case "low":
|
|
4630
|
+
return "info";
|
|
4631
|
+
case "medium":
|
|
4632
|
+
return "warn";
|
|
4633
|
+
case "high":
|
|
4634
|
+
return "error";
|
|
4635
|
+
case "critical":
|
|
4636
|
+
return "critical";
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
var SecurityEventTracker = class {
|
|
4640
|
+
store;
|
|
4641
|
+
defaultSeverity;
|
|
4642
|
+
onEvent;
|
|
4643
|
+
constructor(config) {
|
|
4644
|
+
this.store = config.store;
|
|
4645
|
+
this.defaultSeverity = config.defaultSeverity || "medium";
|
|
4646
|
+
this.onEvent = config.onEvent;
|
|
4647
|
+
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Track a security event
|
|
4650
|
+
*/
|
|
4651
|
+
async track(options) {
|
|
4652
|
+
const severity = options.severity || this.defaultSeverity;
|
|
4653
|
+
const entry = {
|
|
4654
|
+
id: generateId(),
|
|
4655
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
4656
|
+
type: "security",
|
|
4657
|
+
level: severityToLevel(severity),
|
|
4658
|
+
message: options.message,
|
|
4659
|
+
event: options.event,
|
|
4660
|
+
severity,
|
|
4661
|
+
source: options.source || {},
|
|
4662
|
+
target: options.target,
|
|
4663
|
+
details: options.details,
|
|
4664
|
+
mitigated: options.mitigated,
|
|
4665
|
+
metadata: options.metadata
|
|
4666
|
+
};
|
|
4667
|
+
await this.store.write(entry);
|
|
4668
|
+
if (this.onEvent) {
|
|
4669
|
+
await this.onEvent(entry);
|
|
4670
|
+
}
|
|
4671
|
+
return entry;
|
|
4672
|
+
}
|
|
4673
|
+
// Convenience methods for common events
|
|
4674
|
+
/**
|
|
4675
|
+
* Track failed authentication
|
|
4676
|
+
*/
|
|
4677
|
+
async authFailed(options) {
|
|
4678
|
+
return this.track({
|
|
4679
|
+
event: "auth.failed",
|
|
4680
|
+
message: options.reason || "Authentication failed",
|
|
4681
|
+
severity: "medium",
|
|
4682
|
+
source: {
|
|
4683
|
+
ip: options.ip,
|
|
4684
|
+
userAgent: options.userAgent
|
|
4685
|
+
},
|
|
4686
|
+
details: {
|
|
4687
|
+
attemptedEmail: options.email,
|
|
4688
|
+
reason: options.reason
|
|
4689
|
+
},
|
|
4690
|
+
metadata: options.metadata
|
|
4691
|
+
});
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
4694
|
+
* Track successful login
|
|
4695
|
+
*/
|
|
4696
|
+
async authLogin(options) {
|
|
4697
|
+
return this.track({
|
|
4698
|
+
event: "auth.login",
|
|
4699
|
+
message: `User ${options.userId} logged in`,
|
|
4700
|
+
severity: "low",
|
|
4701
|
+
source: {
|
|
4702
|
+
ip: options.ip,
|
|
4703
|
+
userAgent: options.userAgent,
|
|
4704
|
+
userId: options.userId
|
|
4705
|
+
},
|
|
4706
|
+
details: {
|
|
4707
|
+
method: options.method || "credentials"
|
|
4708
|
+
},
|
|
4709
|
+
metadata: options.metadata
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
/**
|
|
4713
|
+
* Track logout
|
|
4714
|
+
*/
|
|
4715
|
+
async authLogout(options) {
|
|
4716
|
+
return this.track({
|
|
4717
|
+
event: "auth.logout",
|
|
4718
|
+
message: `User ${options.userId} logged out`,
|
|
4719
|
+
severity: "low",
|
|
4720
|
+
source: {
|
|
4721
|
+
ip: options.ip,
|
|
4722
|
+
userId: options.userId
|
|
4723
|
+
},
|
|
4724
|
+
details: {
|
|
4725
|
+
reason: options.reason || "user"
|
|
4726
|
+
},
|
|
4727
|
+
metadata: options.metadata
|
|
4728
|
+
});
|
|
4729
|
+
}
|
|
4730
|
+
/**
|
|
4731
|
+
* Track permission denied
|
|
4732
|
+
*/
|
|
4733
|
+
async permissionDenied(options) {
|
|
4734
|
+
return this.track({
|
|
4735
|
+
event: "auth.permission_denied",
|
|
4736
|
+
message: `Permission denied for ${options.action} on ${options.resource}`,
|
|
4737
|
+
severity: "medium",
|
|
4738
|
+
source: {
|
|
4739
|
+
ip: options.ip,
|
|
4740
|
+
userId: options.userId
|
|
4741
|
+
},
|
|
4742
|
+
target: {
|
|
4743
|
+
resource: options.resource,
|
|
4744
|
+
action: options.action
|
|
4745
|
+
},
|
|
4746
|
+
details: {
|
|
4747
|
+
requiredRole: options.requiredRole
|
|
4748
|
+
},
|
|
4749
|
+
metadata: options.metadata
|
|
4750
|
+
});
|
|
4751
|
+
}
|
|
4752
|
+
/**
|
|
4753
|
+
* Track rate limit exceeded
|
|
4754
|
+
*/
|
|
4755
|
+
async rateLimitExceeded(options) {
|
|
4756
|
+
return this.track({
|
|
4757
|
+
event: "ratelimit.exceeded",
|
|
4758
|
+
message: `Rate limit exceeded for ${options.endpoint}`,
|
|
4759
|
+
severity: "medium",
|
|
4760
|
+
source: {
|
|
4761
|
+
ip: options.ip,
|
|
4762
|
+
userId: options.userId
|
|
4763
|
+
},
|
|
4764
|
+
target: {
|
|
4765
|
+
resource: options.endpoint
|
|
4766
|
+
},
|
|
4767
|
+
details: {
|
|
4768
|
+
limit: options.limit,
|
|
4769
|
+
window: options.window
|
|
4770
|
+
},
|
|
4771
|
+
metadata: options.metadata
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
/**
|
|
4775
|
+
* Track CSRF validation failure
|
|
4776
|
+
*/
|
|
4777
|
+
async csrfInvalid(options) {
|
|
4778
|
+
return this.track({
|
|
4779
|
+
event: "csrf.invalid",
|
|
4780
|
+
message: `CSRF validation failed for ${options.endpoint}`,
|
|
4781
|
+
severity: "high",
|
|
4782
|
+
source: {
|
|
4783
|
+
ip: options.ip,
|
|
4784
|
+
userId: options.userId
|
|
4785
|
+
},
|
|
4786
|
+
target: {
|
|
4787
|
+
resource: options.endpoint
|
|
4788
|
+
},
|
|
4789
|
+
details: {
|
|
4790
|
+
reason: options.reason
|
|
4791
|
+
},
|
|
4792
|
+
metadata: options.metadata
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* Track XSS detection
|
|
4797
|
+
*/
|
|
4798
|
+
async xssDetected(options) {
|
|
4799
|
+
return this.track({
|
|
4800
|
+
event: "xss.detected",
|
|
4801
|
+
message: `XSS payload detected in ${options.field}`,
|
|
4802
|
+
severity: "high",
|
|
4803
|
+
source: {
|
|
4804
|
+
ip: options.ip,
|
|
4805
|
+
userId: options.userId
|
|
4806
|
+
},
|
|
4807
|
+
target: {
|
|
4808
|
+
resource: options.endpoint
|
|
4809
|
+
},
|
|
4810
|
+
details: {
|
|
4811
|
+
field: options.field,
|
|
4812
|
+
payload: options.payload?.slice(0, 100)
|
|
4813
|
+
// Truncate
|
|
4814
|
+
},
|
|
4815
|
+
mitigated: true,
|
|
4816
|
+
metadata: options.metadata
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
/**
|
|
4820
|
+
* Track SQL injection detection
|
|
4821
|
+
*/
|
|
4822
|
+
async sqliDetected(options) {
|
|
4823
|
+
return this.track({
|
|
4824
|
+
event: "sqli.detected",
|
|
4825
|
+
message: `SQL injection attempt detected in ${options.field}`,
|
|
4826
|
+
severity: options.severity || "high",
|
|
4827
|
+
source: {
|
|
4828
|
+
ip: options.ip,
|
|
4829
|
+
userId: options.userId
|
|
4830
|
+
},
|
|
4831
|
+
target: {
|
|
4832
|
+
resource: options.endpoint
|
|
4833
|
+
},
|
|
4834
|
+
details: {
|
|
4835
|
+
field: options.field,
|
|
4836
|
+
pattern: options.pattern
|
|
4837
|
+
},
|
|
4838
|
+
mitigated: true,
|
|
4839
|
+
metadata: options.metadata
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Track IP block
|
|
4844
|
+
*/
|
|
4845
|
+
async ipBlocked(options) {
|
|
4846
|
+
return this.track({
|
|
4847
|
+
event: "ip.blocked",
|
|
4848
|
+
message: `IP ${options.ip} blocked: ${options.reason}`,
|
|
4849
|
+
severity: "high",
|
|
4850
|
+
source: {
|
|
4851
|
+
ip: options.ip
|
|
4852
|
+
},
|
|
4853
|
+
details: {
|
|
4854
|
+
reason: options.reason,
|
|
4855
|
+
duration: options.duration
|
|
4856
|
+
},
|
|
4857
|
+
metadata: options.metadata
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
/**
|
|
4861
|
+
* Track suspicious activity
|
|
4862
|
+
*/
|
|
4863
|
+
async suspicious(options) {
|
|
4864
|
+
return this.track({
|
|
4865
|
+
event: "ip.suspicious",
|
|
4866
|
+
message: options.activity,
|
|
4867
|
+
severity: options.severity || "medium",
|
|
4868
|
+
source: {
|
|
4869
|
+
ip: options.ip,
|
|
4870
|
+
userId: options.userId
|
|
4871
|
+
},
|
|
4872
|
+
details: options.details,
|
|
4873
|
+
metadata: options.metadata
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* Track custom event
|
|
4878
|
+
*/
|
|
4879
|
+
async custom(options) {
|
|
4880
|
+
return this.track({
|
|
4881
|
+
event: "custom",
|
|
4882
|
+
...options
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4885
|
+
};
|
|
4886
|
+
function createSecurityTracker(config) {
|
|
4887
|
+
return new SecurityEventTracker(config);
|
|
4888
|
+
}
|
|
4889
|
+
async function trackSecurityEvent(store, options) {
|
|
4890
|
+
const tracker = new SecurityEventTracker({ store });
|
|
4891
|
+
return tracker.track(options);
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
// src/middleware/audit/middleware.ts
|
|
4895
|
+
function generateRequestId() {
|
|
4896
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
4897
|
+
}
|
|
4898
|
+
function getClientIP(req) {
|
|
4899
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || req.headers.get("cf-connecting-ip") || void 0;
|
|
4900
|
+
}
|
|
4901
|
+
function headersToRecord(headers, includeHeaders) {
|
|
4902
|
+
if (!includeHeaders) return {};
|
|
4903
|
+
const result = {};
|
|
4904
|
+
if (includeHeaders === true) {
|
|
4905
|
+
headers.forEach((value, key) => {
|
|
4906
|
+
result[key] = value;
|
|
4907
|
+
});
|
|
4908
|
+
} else if (Array.isArray(includeHeaders)) {
|
|
4909
|
+
for (const key of includeHeaders) {
|
|
4910
|
+
const value = headers.get(key);
|
|
4911
|
+
if (value) result[key] = value;
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
return result;
|
|
4915
|
+
}
|
|
4916
|
+
function parseQuery(url) {
|
|
4917
|
+
const result = {};
|
|
4918
|
+
try {
|
|
4919
|
+
const urlObj = new URL(url);
|
|
4920
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
4921
|
+
result[key] = value;
|
|
4922
|
+
});
|
|
4923
|
+
} catch {
|
|
4924
|
+
}
|
|
4925
|
+
return result;
|
|
4926
|
+
}
|
|
4927
|
+
function statusToLevel(status) {
|
|
4928
|
+
if (status >= 500) return "error";
|
|
4929
|
+
if (status >= 400) return "warn";
|
|
4930
|
+
return "info";
|
|
4931
|
+
}
|
|
4932
|
+
function shouldSkip(req, status, exclude) {
|
|
4933
|
+
if (!exclude) return false;
|
|
4934
|
+
const url = new URL(req.url);
|
|
4935
|
+
if (exclude.paths?.length) {
|
|
4936
|
+
const matchesPath = exclude.paths.some((pattern) => {
|
|
4937
|
+
if (pattern.includes("*")) {
|
|
4938
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
4939
|
+
return regex.test(url.pathname);
|
|
4940
|
+
}
|
|
4941
|
+
return url.pathname === pattern || url.pathname.startsWith(pattern);
|
|
4942
|
+
});
|
|
4943
|
+
if (matchesPath) return true;
|
|
4944
|
+
}
|
|
4945
|
+
if (exclude.methods?.includes(req.method)) {
|
|
4946
|
+
return true;
|
|
4947
|
+
}
|
|
4948
|
+
if (exclude.statusCodes?.includes(status)) {
|
|
4949
|
+
return true;
|
|
4950
|
+
}
|
|
4951
|
+
return false;
|
|
4952
|
+
}
|
|
4953
|
+
function withAuditLog(handler, config) {
|
|
4954
|
+
const {
|
|
4955
|
+
enabled = true,
|
|
4956
|
+
store,
|
|
4957
|
+
include = {},
|
|
4958
|
+
exclude,
|
|
4959
|
+
pii,
|
|
4960
|
+
getUser,
|
|
4961
|
+
requestIdHeader = "x-request-id",
|
|
4962
|
+
generateRequestId: customGenerateId,
|
|
4963
|
+
onError,
|
|
4964
|
+
skip
|
|
4965
|
+
} = config;
|
|
4966
|
+
const includeSettings = {
|
|
4967
|
+
ip: include.ip ?? true,
|
|
4968
|
+
userAgent: include.userAgent ?? true,
|
|
4969
|
+
headers: include.headers ?? false,
|
|
4970
|
+
query: include.query ?? true,
|
|
4971
|
+
body: include.body ?? false,
|
|
4972
|
+
response: include.response ?? true,
|
|
4973
|
+
responseBody: include.responseBody ?? false,
|
|
4974
|
+
duration: include.duration ?? true,
|
|
4975
|
+
user: include.user ?? true
|
|
4976
|
+
};
|
|
4977
|
+
const piiConfig = pii || {
|
|
4978
|
+
fields: DEFAULT_PII_FIELDS,
|
|
4979
|
+
mode: "mask"
|
|
4980
|
+
};
|
|
4981
|
+
return async (req) => {
|
|
4982
|
+
if (!enabled) {
|
|
4983
|
+
return handler(req);
|
|
4984
|
+
}
|
|
4985
|
+
if (skip && await skip(req)) {
|
|
4986
|
+
return handler(req);
|
|
4987
|
+
}
|
|
4988
|
+
const startTime = Date.now();
|
|
4989
|
+
const requestId = req.headers.get(requestIdHeader) || (customGenerateId ? customGenerateId() : generateRequestId());
|
|
4990
|
+
const url = new URL(req.url);
|
|
4991
|
+
let requestInfo = {
|
|
4992
|
+
id: requestId,
|
|
4993
|
+
method: req.method,
|
|
4994
|
+
url: req.url,
|
|
4995
|
+
path: url.pathname
|
|
4996
|
+
};
|
|
4997
|
+
if (includeSettings.ip) {
|
|
4998
|
+
requestInfo.ip = getClientIP(req);
|
|
4999
|
+
}
|
|
5000
|
+
if (includeSettings.userAgent) {
|
|
5001
|
+
requestInfo.userAgent = req.headers.get("user-agent") || void 0;
|
|
5002
|
+
}
|
|
5003
|
+
if (includeSettings.headers) {
|
|
5004
|
+
let headers = headersToRecord(req.headers, includeSettings.headers);
|
|
5005
|
+
headers = redactHeaders(headers);
|
|
5006
|
+
requestInfo.headers = headers;
|
|
5007
|
+
}
|
|
5008
|
+
if (includeSettings.query) {
|
|
5009
|
+
let query = parseQuery(req.url);
|
|
5010
|
+
query = redactQuery(query);
|
|
5011
|
+
requestInfo.query = query;
|
|
5012
|
+
}
|
|
5013
|
+
requestInfo.contentType = req.headers.get("content-type") || void 0;
|
|
5014
|
+
requestInfo.contentLength = parseInt(req.headers.get("content-length") || "0", 10) || void 0;
|
|
5015
|
+
let user;
|
|
5016
|
+
if (includeSettings.user && getUser) {
|
|
5017
|
+
try {
|
|
5018
|
+
user = await getUser(req) || void 0;
|
|
5019
|
+
} catch {
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
let response;
|
|
5023
|
+
let error;
|
|
5024
|
+
try {
|
|
5025
|
+
response = await handler(req);
|
|
5026
|
+
} catch (err) {
|
|
5027
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
5028
|
+
throw err;
|
|
5029
|
+
} finally {
|
|
5030
|
+
const duration = Date.now() - startTime;
|
|
5031
|
+
const status = response?.status || 500;
|
|
5032
|
+
if (!shouldSkip(req, status, exclude)) {
|
|
5033
|
+
const entry = {
|
|
5034
|
+
id: requestId,
|
|
5035
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5036
|
+
type: "request",
|
|
5037
|
+
level: error ? "error" : statusToLevel(status),
|
|
5038
|
+
message: `${req.method} ${url.pathname} ${status} ${duration}ms`,
|
|
5039
|
+
request: requestInfo,
|
|
5040
|
+
user
|
|
5041
|
+
};
|
|
5042
|
+
if (includeSettings.response && response) {
|
|
5043
|
+
entry.response = {
|
|
5044
|
+
status: response.status,
|
|
5045
|
+
duration
|
|
5046
|
+
};
|
|
5047
|
+
if (includeSettings.headers) {
|
|
5048
|
+
entry.response.headers = headersToRecord(response.headers, includeSettings.headers);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
if (error) {
|
|
5052
|
+
entry.error = {
|
|
5053
|
+
name: error.name,
|
|
5054
|
+
message: error.message,
|
|
5055
|
+
stack: error.stack
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
const redactedEntry = redactObject(entry, piiConfig);
|
|
5059
|
+
try {
|
|
5060
|
+
await store.write(redactedEntry);
|
|
5061
|
+
} catch (writeError) {
|
|
5062
|
+
if (onError) {
|
|
5063
|
+
onError(writeError instanceof Error ? writeError : new Error(String(writeError)), entry);
|
|
5064
|
+
} else {
|
|
5065
|
+
console.error("[AuditLog] Failed to write log:", writeError);
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
return response;
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
function createAuditMiddleware(config) {
|
|
5074
|
+
return (handler) => withAuditLog(handler, config);
|
|
5075
|
+
}
|
|
5076
|
+
function withRequestId(handler, options = {}) {
|
|
5077
|
+
const { headerName = "x-request-id", generateId: generateId2 = generateRequestId } = options;
|
|
5078
|
+
return async (req) => {
|
|
5079
|
+
const requestId = req.headers.get(headerName) || generateId2();
|
|
5080
|
+
const response = await handler(req);
|
|
5081
|
+
const newResponse = new Response(response.body, {
|
|
5082
|
+
status: response.status,
|
|
5083
|
+
statusText: response.statusText,
|
|
5084
|
+
headers: new Headers(response.headers)
|
|
5085
|
+
});
|
|
5086
|
+
newResponse.headers.set(headerName, requestId);
|
|
5087
|
+
return newResponse;
|
|
5088
|
+
};
|
|
5089
|
+
}
|
|
5090
|
+
function withTiming(handler, options = {}) {
|
|
5091
|
+
const { headerName = "x-response-time", log = false } = options;
|
|
5092
|
+
return async (req) => {
|
|
5093
|
+
const start = Date.now();
|
|
5094
|
+
const response = await handler(req);
|
|
5095
|
+
const duration = Date.now() - start;
|
|
5096
|
+
const newResponse = new Response(response.body, {
|
|
5097
|
+
status: response.status,
|
|
5098
|
+
statusText: response.statusText,
|
|
5099
|
+
headers: new Headers(response.headers)
|
|
5100
|
+
});
|
|
5101
|
+
newResponse.headers.set(headerName, `${duration}ms`);
|
|
5102
|
+
if (log) {
|
|
5103
|
+
const url = new URL(req.url);
|
|
5104
|
+
console.log(`${req.method} ${url.pathname} ${response.status} ${duration}ms`);
|
|
5105
|
+
}
|
|
5106
|
+
return newResponse;
|
|
5107
|
+
};
|
|
5108
|
+
}
|
|
1449
5109
|
|
|
1450
5110
|
// src/index.ts
|
|
1451
|
-
var VERSION = "0.
|
|
5111
|
+
var VERSION = "0.6.0";
|
|
1452
5112
|
|
|
5113
|
+
exports.AuditMemoryStore = MemoryStore2;
|
|
1453
5114
|
exports.AuthenticationError = AuthenticationError;
|
|
1454
5115
|
exports.AuthorizationError = AuthorizationError;
|
|
5116
|
+
exports.CLFFormatter = CLFFormatter;
|
|
1455
5117
|
exports.ConfigurationError = ConfigurationError;
|
|
5118
|
+
exports.ConsoleStore = ConsoleStore;
|
|
1456
5119
|
exports.CsrfError = CsrfError;
|
|
5120
|
+
exports.DANGEROUS_EXTENSIONS = DANGEROUS_EXTENSIONS;
|
|
5121
|
+
exports.DEFAULT_PII_FIELDS = DEFAULT_PII_FIELDS;
|
|
5122
|
+
exports.ExternalStore = ExternalStore;
|
|
5123
|
+
exports.JSONFormatter = JSONFormatter;
|
|
5124
|
+
exports.MIME_TYPES = MIME_TYPES;
|
|
1457
5125
|
exports.MemoryStore = MemoryStore;
|
|
5126
|
+
exports.MultiStore = MultiStore;
|
|
1458
5127
|
exports.PRESET_API = PRESET_API;
|
|
1459
5128
|
exports.PRESET_RELAXED = PRESET_RELAXED;
|
|
1460
5129
|
exports.PRESET_STRICT = PRESET_STRICT;
|
|
1461
5130
|
exports.RateLimitError = RateLimitError;
|
|
1462
5131
|
exports.SecureError = SecureError;
|
|
5132
|
+
exports.SecurityEventTracker = SecurityEventTracker;
|
|
5133
|
+
exports.StructuredFormatter = StructuredFormatter;
|
|
5134
|
+
exports.TextFormatter = TextFormatter;
|
|
1463
5135
|
exports.VERSION = VERSION;
|
|
1464
5136
|
exports.ValidationError = ValidationError;
|
|
1465
5137
|
exports.anonymizeIp = anonymizeIp;
|
|
@@ -1468,11 +5140,30 @@ exports.buildHSTS = buildHSTS;
|
|
|
1468
5140
|
exports.buildPermissionsPolicy = buildPermissionsPolicy;
|
|
1469
5141
|
exports.checkRateLimit = checkRateLimit;
|
|
1470
5142
|
exports.clearAllRateLimits = clearAllRateLimits;
|
|
5143
|
+
exports.createAuditMemoryStore = createMemoryStore2;
|
|
5144
|
+
exports.createAuditMiddleware = createAuditMiddleware;
|
|
5145
|
+
exports.createCLFFormatter = createCLFFormatter;
|
|
1471
5146
|
exports.createCSRFToken = createToken;
|
|
5147
|
+
exports.createConsoleStore = createConsoleStore;
|
|
5148
|
+
exports.createDatadogStore = createDatadogStore;
|
|
5149
|
+
exports.createExternalStore = createExternalStore;
|
|
5150
|
+
exports.createJSONFormatter = createJSONFormatter;
|
|
1472
5151
|
exports.createMemoryStore = createMemoryStore;
|
|
5152
|
+
exports.createMultiStore = createMultiStore;
|
|
1473
5153
|
exports.createRateLimiter = createRateLimiter;
|
|
5154
|
+
exports.createRedactor = createRedactor;
|
|
1474
5155
|
exports.createSecurityHeaders = createSecurityHeaders;
|
|
1475
5156
|
exports.createSecurityHeadersObject = createSecurityHeadersObject;
|
|
5157
|
+
exports.createSecurityTracker = createSecurityTracker;
|
|
5158
|
+
exports.createStructuredFormatter = createStructuredFormatter;
|
|
5159
|
+
exports.createTextFormatter = createTextFormatter;
|
|
5160
|
+
exports.createValidator = createValidator;
|
|
5161
|
+
exports.decodeJWT = decodeJWT;
|
|
5162
|
+
exports.detectFileType = detectFileType;
|
|
5163
|
+
exports.detectSQLInjection = detectSQLInjection;
|
|
5164
|
+
exports.detectXSS = detectXSS;
|
|
5165
|
+
exports.escapeHtml = escapeHtml;
|
|
5166
|
+
exports.extractBearerToken = extractBearerToken;
|
|
1476
5167
|
exports.formatDuration = formatDuration;
|
|
1477
5168
|
exports.generateCSRF = generateCSRF;
|
|
1478
5169
|
exports.getClientIp = getClientIp;
|
|
@@ -1480,22 +5171,67 @@ exports.getGeoInfo = getGeoInfo;
|
|
|
1480
5171
|
exports.getGlobalMemoryStore = getGlobalMemoryStore;
|
|
1481
5172
|
exports.getPreset = getPreset;
|
|
1482
5173
|
exports.getRateLimitStatus = getRateLimitStatus;
|
|
5174
|
+
exports.hasPathTraversal = hasPathTraversal;
|
|
5175
|
+
exports.hasSQLInjection = hasSQLInjection;
|
|
5176
|
+
exports.isFormRequest = isFormRequest;
|
|
5177
|
+
exports.isJsonRequest = isJsonRequest;
|
|
1483
5178
|
exports.isLocalhost = isLocalhost;
|
|
1484
5179
|
exports.isPrivateIp = isPrivateIp;
|
|
1485
5180
|
exports.isSecureError = isSecureError;
|
|
1486
5181
|
exports.isValidIp = isValidIp;
|
|
5182
|
+
exports.mask = mask;
|
|
1487
5183
|
exports.normalizeIp = normalizeIp;
|
|
1488
5184
|
exports.nowInMs = nowInMs;
|
|
1489
5185
|
exports.nowInSeconds = nowInSeconds;
|
|
1490
5186
|
exports.parseDuration = parseDuration;
|
|
5187
|
+
exports.redactCreditCard = redactCreditCard;
|
|
5188
|
+
exports.redactEmail = redactEmail;
|
|
5189
|
+
exports.redactHeaders = redactHeaders;
|
|
5190
|
+
exports.redactIP = redactIP;
|
|
5191
|
+
exports.redactObject = redactObject;
|
|
5192
|
+
exports.redactPhone = redactPhone;
|
|
5193
|
+
exports.redactQuery = redactQuery;
|
|
1491
5194
|
exports.resetRateLimit = resetRateLimit;
|
|
5195
|
+
exports.sanitize = sanitize;
|
|
5196
|
+
exports.sanitizeFields = sanitizeFields;
|
|
5197
|
+
exports.sanitizeFilename = sanitizeFilename;
|
|
5198
|
+
exports.sanitizeObject = sanitizeObject;
|
|
5199
|
+
exports.sanitizePath = sanitizePath;
|
|
5200
|
+
exports.sanitizeSQLInput = sanitizeSQLInput;
|
|
1492
5201
|
exports.sleep = sleep;
|
|
5202
|
+
exports.stripHtml = stripHtml;
|
|
1493
5203
|
exports.toSecureError = toSecureError;
|
|
1494
5204
|
exports.tokensMatch = tokensMatch;
|
|
5205
|
+
exports.trackSecurityEvent = trackSecurityEvent;
|
|
5206
|
+
exports.validate = validate;
|
|
5207
|
+
exports.validateBody = validateBody;
|
|
1495
5208
|
exports.validateCSRF = validateCSRF;
|
|
5209
|
+
exports.validateContentType = validateContentType;
|
|
5210
|
+
exports.validateFile = validateFile;
|
|
5211
|
+
exports.validateFiles = validateFiles;
|
|
5212
|
+
exports.validatePath = validatePath;
|
|
5213
|
+
exports.validateQuery = validateQuery;
|
|
5214
|
+
exports.validateRequest = validateRequest;
|
|
1496
5215
|
exports.verifyCSRFToken = verifyToken;
|
|
5216
|
+
exports.verifyJWT = verifyJWT;
|
|
5217
|
+
exports.withAPIKey = withAPIKey;
|
|
5218
|
+
exports.withAuditLog = withAuditLog;
|
|
5219
|
+
exports.withAuth = withAuth;
|
|
1497
5220
|
exports.withCSRF = withCSRF;
|
|
5221
|
+
exports.withContentType = withContentType;
|
|
5222
|
+
exports.withFileValidation = withFileValidation;
|
|
5223
|
+
exports.withJWT = withJWT;
|
|
5224
|
+
exports.withOptionalAuth = withOptionalAuth;
|
|
1498
5225
|
exports.withRateLimit = withRateLimit;
|
|
5226
|
+
exports.withRequestId = withRequestId;
|
|
5227
|
+
exports.withRoles = withRoles;
|
|
5228
|
+
exports.withSQLProtection = withSQLProtection;
|
|
5229
|
+
exports.withSanitization = withSanitization;
|
|
5230
|
+
exports.withSecureValidation = withSecureValidation;
|
|
1499
5231
|
exports.withSecurityHeaders = withSecurityHeaders;
|
|
5232
|
+
exports.withSession = withSession;
|
|
5233
|
+
exports.withTiming = withTiming;
|
|
5234
|
+
exports.withValidation = withValidation;
|
|
5235
|
+
exports.withXSSProtection = withXSSProtection;
|
|
1500
5236
|
//# sourceMappingURL=index.cjs.map
|
|
1501
5237
|
//# sourceMappingURL=index.cjs.map
|