fix: Add strict ownership validation for claim modifications

Add comprehensive security checks to prevent unauthorized claim modifications
while preserving legitimate owner's ability to update their own claims.

- Add ownership verification before any pass.remove() operations
- Validate claimKey format: claim:domain:claimant
- Ensure claimant matches current user before allowing modifications
- Create safeRemoveClaim() helper with multi-layer validation
- Add security logging for audit trail
- Fail-safe error handling prevents unauthorized claim interference

Resolves critical data integrity issue where claims could be overwritten
by unauthorized peers, while maintaining proper claim management capabilities.
This commit is contained in:
Raven Scott
2025-12-26 17:09:16 -05:00
parent 0d23e296be
commit 7a9889befe
2 changed files with 67 additions and 13 deletions
+51 -5
View File
@@ -162,16 +162,29 @@ async function updateClaimClients(domain, claimant, clients) {
// Parse existing claim
const parsed = parseClaimValue(existingClaim.value);
// CRITICAL: Verify ownership before modifying
const claimKeyParts = claimKey.split(':');
if (claimKeyParts.length !== 3 || claimKeyParts[0] !== 'claim' || claimKeyParts[1] !== domain || claimKeyParts[2] !== claimant) {
logError('Core', `SECURITY: Claim key ownership verification failed for ${claimKey}`);
return false;
}
// Verify the entry exists and belongs to us
if (!existingClaim || existingClaim.key !== claimKey) {
logWarn('Core', `Cannot update claim: entry not found or ownership mismatch`);
return false;
}
// Update with new clients array
const updatedValue = JSON.stringify({
hash: parsed.hash,
clients: clients,
timestamp: parsed.timestamp || Date.now()
});
// Remove old claim and add updated one
await pass.remove(claimKey);
// SAFE: Remove old claim and add updated one (ownership verified)
await safeRemoveClaim(claimKey, claimant);
await pass.add(claimKey, updatedValue);
invalidateEntriesCache();
@@ -745,6 +758,38 @@ async function removeOwnClaimAndVotes(domain, localWriter) {
}
}
// Helper function for safe claim removal with ownership verification
async function safeRemoveClaim(claimKey, expectedClaimant) {
// Parse and validate claimKey format
const parts = claimKey.split(':');
if (parts.length !== 3 || parts[0] !== 'claim') {
throw new Error(`Invalid claim key format: ${claimKey}`);
}
const domain = parts[1];
const claimant = parts[2];
// CRITICAL: Verify the claimant matches expected
if (claimant !== expectedClaimant) {
logError('Core', `SECURITY: Attempted to remove claim with wrong claimant. Expected ${expectedClaimant}, got ${claimant}`);
throw new Error(`Cannot remove claim: ownership mismatch`);
}
// Verify entry exists before removing
const allEntries = await getAllEntries(state.dnsPass, false);
const entryExists = allEntries.some(e => e.key === claimKey);
if (!entryExists) {
logWarn('Core', `Claim ${claimKey} does not exist, skipping remove`);
return false;
}
// Safe to remove - ownership verified
await state.dnsPass.remove(claimKey);
logInfo('Core', `Safely removed claim ${claimKey} (owned by ${claimant})`);
return true;
}
// Remove all records (claims and votes) from the network
async function removeAllRecords() {
const pass = state.dnsPass;
@@ -853,5 +898,6 @@ module.exports = {
parseClaimValue,
getClaimClients,
updateClaimClients,
cleanupRedundantClientEntries
cleanupRedundantClientEntries,
safeRemoveClaim
};
+16 -8
View File
@@ -1,6 +1,6 @@
const state = require('../infrastructure/state');
const { logDebug, logInfo } = require('../infrastructure/logger');
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache, parseClaimValue } = require('./core');
const { getAllEntries, autoVoteForDomain, invalidateEntriesCache, parseClaimValue, safeRemoveClaim } = require('./core');
const { trackDomainEvent } = require('../maintenance/metrics');
const { getPersistentPublicKey } = require('../infrastructure/utils');
// Initialize reserved IPs set if not already present
@@ -35,21 +35,29 @@ async function addDomain(domain, hash, ssl = false) {
const claimValue = JSON.stringify({ hash, timestamp, ssl: sslValue });
if (existingClaim) {
// User already has a claim - update it instead of creating duplicate
// User already has a claim - verify ownership before modifying
const parsed = parseClaimValue(existingClaim.value);
// CRITICAL: Verify the claimKey matches our claimant (double-check ownership)
const claimKeyParts = existingClaim.key.split(':');
if (claimKeyParts.length !== 3 || claimKeyParts[0] !== 'claim' || claimKeyParts[1] !== domain || claimKeyParts[2] !== claimant) {
logError('Domains', `SECURITY: Claim key mismatch! Expected ${claimKey}, got ${existingClaim.key}. Refusing to modify.`);
throw new Error(`Cannot modify claim: key ownership verification failed`);
}
// Check if anything actually changed
if (parsed.hash === hash && parsed.ssl === sslValue) {
logDebug('Domains', `Claim for ${domain} already exists with same hash and SSL flag, skipping update`);
return; // No changes needed
}
logInfo('Domains', `Updating existing claim for ${domain} (hash: ${parsed.hash} -> ${hash}, SSL: ${parsed.ssl} -> ${sslValue})`);
// Remove old claim and add updated one
await pass.remove(claimKey);
// SAFE: Remove old claim and add updated one (ownership verified above)
// Use the safe remove helper for consistent validation
await safeRemoveClaim(claimKey, claimant);
await pass.add(claimKey, claimValue);
logInfo('Domains', `Updated claim ${claimKey} with new hash ${hash} and SSL=${sslValue}`);
} else {
// New claim - add it