תקיפת תהליכים מרובי שלבים - Multi-Step Process Attacks¶
מהן חולשות בתהליכים מרובי שלבים¶
תהליכים מרובי שלבים (Multi-Step Processes) הם תהליכים שבהם המשתמש עובר דרך מספר שלבים לפני השלמת פעולה - הרשמה, רכישה, שינוי סיסמה, אישור עסקה, ועוד. חולשות בתהליכים אלו נוצרות כאשר השרת לא מוודא שהמשתמש עבר את כל השלבים בסדר הנכון, או כאשר הנתונים שעוברים בין השלבים ניתנים למניפולציה.
דילוג על שלבים - Step Skipping¶
הבעיה הבסיסית¶
תהליך רכישה תקין:
Step 1: POST /checkout/cart -> cart review
Step 2: POST /checkout/shipping -> enter shipping address
Step 3: POST /checkout/payment -> enter payment details
Step 4: POST /checkout/confirm -> final confirmation
תקיפה - דילוג ישירות לאישור:
Step 1: POST /checkout/cart -> cart review
Step 4: POST /checkout/confirm -> confirm with no payment!
קוד פגיע¶
// Step 1 - cart review
app.post('/checkout/cart', (req, res) => {
req.session.cart = getCart(req.session.userId);
res.json({ items: req.session.cart.items, total: req.session.cart.total });
});
// Step 2 - shipping address
app.post('/checkout/shipping', (req, res) => {
req.session.shippingAddress = req.body.address;
res.json({ shippingCost: calculateShipping(req.body.address) });
});
// Step 3 - payment
app.post('/checkout/payment', (req, res) => {
const result = processPayment(req.body.cardDetails, req.session.cart.total);
req.session.paymentConfirmed = result.success;
res.json({ paymentId: result.id });
});
// Step 4 - confirmation (vulnerable!)
app.post('/checkout/confirm', (req, res) => {
// Vulnerable - doesn't check that payment was made
const order = createOrder({
userId: req.session.userId,
items: req.session.cart.items,
address: req.session.shippingAddress
});
clearCart(req.session.userId);
res.json({ orderId: order.id, message: 'Order confirmed!' });
});
התוקף שולח POST /checkout/confirm ישירות אחרי שלב 1, בלי לעבור דרך שלב התשלום.
מניפולציית פרמטרים בין שלבים¶
שינוי פרמטרים שעוברים בין שלבים¶
Step 1: review -> the server returns a summary with total=1337
Step 2: payment -> the client sends total=1337 back
Step 3: confirm -> the server confirms based on the submitted total
תקיפה:
-- original step 2
POST /checkout/payment HTTP/1.1
Content-Type: application/x-www-form-urlencoded
total=1337&cardNumber=4111111111111111&expiry=12/28
-- modified step 2
POST /checkout/payment HTTP/1.1
Content-Type: application/x-www-form-urlencoded
total=1&cardNumber=4111111111111111&expiry=12/28
קוד פגיע:
@app.route('/checkout/payment', methods=['POST'])
def process_payment():
# Vulnerable - trusts the amount from the client
total = float(request.form['total'])
card = request.form['cardNumber']
# Charges the card the amount sent by the client
result = charge_card(card, total)
if result['success']:
session['payment_amount'] = total # stores the wrong amount
return redirect('/checkout/confirm')
תקיפת שידור חוזר - Replay Attacks¶
שימוש חוזר בתגובת שלב מוצלח¶
Valid process:
1. POST /transfer -> transfer request
2. Receive confirmation token: token=abc123
3. POST /transfer/confirm?token=abc123 -> confirm the transfer
Attack - reusing the token:
3. POST /transfer/confirm?token=abc123 -> first transfer (successful)
4. POST /transfer/confirm?token=abc123 -> second transfer (reuse!)
5. POST /transfer/confirm?token=abc123 -> third transfer...
קוד פגיע:
app.post('/transfer/confirm', (req, res) => {
const { token } = req.query;
// Vulnerable - checks that the token is valid but doesn't invalidate it
const transfer = pendingTransfers.find(t => t.token === token);
if (!transfer) {
return res.status(400).json({ error: 'Invalid token' });
}
// Performs the transfer
executeTransfer(transfer);
// but doesn't remove the token from the list!
res.json({ success: true });
});
הbypass מעקב שלבים מבוסס Session¶
מניפולציית משתנה session¶
# The server tracks the current step in the session
@app.route('/wizard/step1', methods=['POST'])
def step1():
session['current_step'] = 1
session['data_step1'] = request.form.to_dict()
return redirect('/wizard/step2')
@app.route('/wizard/step2', methods=['POST'])
def step2():
if session.get('current_step') != 1:
return redirect('/wizard/step1')
session['current_step'] = 2
session['data_step2'] = request.form.to_dict()
return redirect('/wizard/step3')
@app.route('/wizard/step3', methods=['POST'])
def step3():
if session.get('current_step') != 2:
return redirect('/wizard/step1')
# Performs the action
process_wizard(session['data_step1'], session['data_step2'], request.form)
הבעיה: מה קורה אם התוקף פותח שני חלונות?
Window 1: step1 -> step2 (session['current_step'] = 2)
Window 2: step1 -> step2 (session['current_step'] = 2, overwrites window 1)
Window 1: step3 -> succeeds! (current_step == 2)
but data_step1 and data_step2 are from window 2!
שימוש חוזר בטוקנים בין שלבים¶
בעיית טוקן לא ייחודי¶
Password change process:
1. POST /change-password/verify -> sends an SMS code
2. POST /change-password/confirm?code=123456 -> verifies the code
3. POST /change-password/set -> sets a new password
תקיפה - שינוי הנמען בין שלב 2 ל-3:
-- step 1: password change request for our account
POST /change-password/verify HTTP/1.1
Cookie: session=my_session
{"email": "attacker@evil.com"}
-- step 2: verify the code (the code we received via SMS)
POST /change-password/confirm HTTP/1.1
Cookie: session=my_session
{"code": "123456"}
-- step 3: change the parameter to the victim's account!
POST /change-password/set HTTP/1.1
Cookie: session=my_session
{"email": "victim@example.com", "newPassword": "hacked123"}
קוד פגיע:
app.post('/change-password/set', (req, res) => {
// Checks that the user passed verification
if (!req.session.passwordVerified) {
return res.status(403).json({ error: 'Not verified' });
}
// Vulnerable - takes the email from the current request
// instead of from the session where verification took place
const email = req.body.email;
const newPassword = req.body.newPassword;
updatePassword(email, newPassword);
req.session.passwordVerified = false;
res.json({ success: true });
});
הbypass אימות מרובה שלבים¶
דילוג על שלב 2FA¶
Login process:
1. POST /login -> authenticate username and password
2. POST /login/2fa -> enter the OTP code
3. GET /dashboard -> access the system
תקיפה:
-- step 1: normal login
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=victim&password=password123
HTTP/1.1 302 Found
Location: /login/2fa
Set-Cookie: session=abc123
-- skip step 2 - direct access
GET /dashboard HTTP/1.1
Cookie: session=abc123
HTTP/1.1 200 OK
-- if the server doesn't check that 2FA was completed, we get access
קוד פגיע:
@app.route('/login', methods=['POST'])
def login():
user = authenticate(request.form['username'], request.form['password'])
if user:
session['user_id'] = user.id
session['authenticated'] = True
# Vulnerable - sets authenticated=True before 2FA
if user.has_2fa:
return redirect('/login/2fa')
return redirect('/dashboard')
@app.route('/dashboard')
@login_required # only checks authenticated=True
def dashboard():
return render_template('dashboard.html')
מניפולציית session בין משתמשים¶
-- log in as a regular user (no 2FA)
POST /login HTTP/1.1
username=wiener&password=peter
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: session=sess_wiener
-- switch to the victim's session via the 2FA step
POST /login HTTP/1.1
username=victim&password=victimpass
HTTP/1.1 302 Found
Location: /login/2fa
Set-Cookie: session=sess_victim
-- use the victim's session (already authenticated)
-- with a modified user parameter
POST /login/2fa HTTP/1.1
Cookie: session=sess_victim
code=000000&username=victim
-- if the server doesn't check the code properly
-- or if the username can be changed in the session
מניפולציית תהליך רכישה¶
שינוי מוצר אחרי תשלום¶
Step 1: add a cheap product (USB cable - $5) to the cart
Step 2: pay $5
Step 3: change the product in the cart to a laptop ($1500) before confirming
Step 4: confirm - receive the laptop for $5
-- step 2: payment
POST /checkout/payment HTTP/1.1
Content-Type: application/json
{"cartId": "cart_123", "amount": 5}
HTTP/1.1 200 OK
{"paymentId": "pay_456", "status": "success"}
-- between step 2 and 3: change the product in the cart
PUT /cart/items/1 HTTP/1.1
Content-Type: application/json
{"productId": 42, "quantity": 1}
-- step 3: confirm
POST /checkout/confirm HTTP/1.1
Content-Type: application/json
{"paymentId": "pay_456", "cartId": "cart_123"}
הגנות¶
ולידציית שלבים בצד שרת¶
// Secure step-tracking mechanism
class CheckoutProcess {
constructor(userId) {
this.userId = userId;
this.steps = {
cart: { completed: false, data: null },
shipping: { completed: false, data: null },
payment: { completed: false, data: null },
confirm: { completed: false, data: null }
};
this.currentStep = 'cart';
}
canProceedTo(step) {
const stepOrder = ['cart', 'shipping', 'payment', 'confirm'];
const targetIndex = stepOrder.indexOf(step);
// Check that all previous steps were completed
for (let i = 0; i < targetIndex; i++) {
if (!this.steps[stepOrder[i]].completed) {
return false;
}
}
return true;
}
}
app.post('/checkout/confirm', (req, res) => {
const process = getCheckoutProcess(req.session.userId);
// Protected - check that all previous steps were completed
if (!process.canProceedTo('confirm')) {
return res.status(400).json({
error: 'Please complete all previous steps'
});
}
// Additional check that the payment matches the current cart
if (process.steps.payment.data.amount !== process.steps.cart.data.total) {
return res.status(400).json({
error: 'Payment amount mismatch'
});
}
// Confirm the order
createOrder(process);
process.steps.confirm.completed = true;
});
טוקנים קריפטוגרפיים לכל שלב¶
const crypto = require('crypto');
function generateStepToken(userId, step, data) {
const payload = JSON.stringify({ userId, step, data, timestamp: Date.now() });
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(payload);
return {
payload: Buffer.from(payload).toString('base64'),
signature: hmac.digest('hex')
};
}
function verifyStepToken(token, expectedStep) {
const { payload, signature } = token;
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(Buffer.from(payload, 'base64').toString());
if (hmac.digest('hex') !== signature) {
return null; // Invalid signature
}
const data = JSON.parse(Buffer.from(payload, 'base64').toString());
if (data.step !== expectedStep) {
return null; // Step mismatch
}
// Check time validity
if (Date.now() - data.timestamp > 30 * 60 * 1000) {
return null; // Expired (30 minutes)
}
return data;
}
ביטול טוקנים חד-פעמיים¶
app.post('/transfer/confirm', async (req, res) => {
const { token } = req.body;
// Protected - atomic operation: check + delete
const transfer = await db.query(
'DELETE FROM pending_transfers WHERE token = ? RETURNING *',
[token]
);
if (!transfer) {
return res.status(400).json({ error: 'Invalid or used token' });
}
// The token was deleted - can't be reused
await executeTransfer(transfer);
res.json({ success: true });
});
נעילת נתונים אחרי תשלום¶
app.post('/checkout/payment', async (req, res) => {
const cart = await getCart(req.session.userId);
// Create a snapshot of the cart at the moment of payment
const snapshot = {
items: JSON.parse(JSON.stringify(cart.items)),
total: cart.total,
hash: hashCart(cart)
};
const payment = await processPayment(req.body.cardDetails, snapshot.total);
if (payment.success) {
// Store the snapshot - not the cart itself
req.session.paymentSnapshot = snapshot;
req.session.paymentId = payment.id;
}
});
app.post('/checkout/confirm', async (req, res) => {
const snapshot = req.session.paymentSnapshot;
const currentCart = await getCart(req.session.userId);
// Check that the cart hasn't changed since payment
if (hashCart(currentCart) !== snapshot.hash) {
return res.status(400).json({
error: 'Cart changed since payment. Please pay again.'
});
}
// Create the order based on the snapshot
createOrder(snapshot);
});
סיכום¶
תקיפת תהליכים מרובי שלבים מנצלת את העובדה שמפתחים מניחים שהמשתמש ילך בסדר הנכון. הנקודות המרכזיות:
- תמיד ודאו בצד השרת שכל השלבים הקודמים הושלמו
- אל תסמכו על פרמטרים שנשלחים מהלקוח בין שלבים
- השתמשו בטוקנים חד-פעמיים ומחקו אותם אחרי שימוש
- נעלו את הנתונים ברגע התשלום ובדקו שלא השתנו
- בדקו את הזהות בכל שלב - לא רק בשלב הראשון
- הגדירו timeout לתהליכים - שלא יישארו פתוחים לנצח