Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
🛠️ PHP + MySQL REST API  ·  Lesson 12

API Security और Testing

बड़े ख़तरे और हमारा बचाव

Android app को untrusted मानें: कोई भी उसकी requests copy करके बदल सकता है। जो भी ज़रूरी है वह server पर तय होता है।

ख़तराExampleKartEasy बचावकहाँ
SQL injectionq=' OR 1=1 --PDO prepared statements, whitelisted ORDER BYहर endpoint
IDORदूसरे user का order id=1 खोलनाहर query में AND user_id = ?cart, address, orders
Price tampering"price": 1 भेजनाprices और totals सिर्फ database सेcart.php, place.php
Oversellingदो खरीदार, एक itemtransaction + SELECT … FOR UPDATEorders/place.php
चोरी हुआ password DBbackup leakpassword_hash, tokens SHA-256 मेंauth, user_tokens
Fake reviewsबिना खरीदे reviewसिर्फ delivered ordersreviews/add.php
Admin में XSSname = <script>हर output पर h()admin/*
Admin में CSRFदूसरी site पर hidden formCSRF token + hash_equalsadmin/*
ख़तरनाक uploadshell.php.jpggetimagesize, random name, uploads में PHP नहींadmin/inc/bootstrap.php
Info leakusers को SQL error दिखनाDEBUG off, errors सिर्फ log मेंhelpers.php
Terminal – price में धोखा करने की कोशिश
# Attacker tries to buy at ₹1 by sending a price
curl -X POST $B/cart/add.php -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"product_id":7,"qty":1,"price":1}'
"price" is simply ignored: add.php reads only product_id and qty. The cart still shows "price": 1899 from the products table.
Terminal – SQL injection की कोशिश
# SQL injection attempt in the search box
curl "$B/products.php?q=' OR '1'='1&sort=price_asc;DROP TABLE users"
HTTP 200 {"success":true,"message":"OK","data":{"items":[],"page":1,"limit":20,"total":0,"has_more":false}} → the text is searched literally (placeholders) and the unknown sort falls back to "newest".

Security checklist

  1. 1Domain पर HTTPS (free SSL), BASE_URL https:// से शुरू
  2. 2config.php में DEBUG false
  3. 3Admin password बदला; चाहें तो admin folder का नाम बदलें
  4. 4includes/, tests/ और database.sql browser में 403 दें
  5. 5Admin panel में .php file upload मना हो
  6. 6id use करने वाली हर customer query user_id भी check करे
  7. 7Smoke test: 26 passed, 0 failed
App publish करने से पहले सातों tick करें

Errors और logs

कुछ टूटने पर users को friendly message दिखता है और आपको error log में असली वजह (Hostinger: hPanel → Advanced → Error logs, या folder में error_log file)।

PHP – includes/helpers.php
<?php
// helpers.php: every uncaught error
set_exception_handler(function ($e) {
    error_log('[KartEasy] ' . $e->getMessage());          // goes to the server error log
    $msg = DEBUG ? $e->getMessage() : 'Something went wrong. Please try again.';
    json_out(array('success' => false, 'message' => $msg), 500);
});

DEBUG = true (आपका PC)

  • JSON में असली message
  • जैसे "Unknown column 'prise'"
  • bugs जल्दी fix

DEBUG = false (live)

  • "Something went wrong."
  • details सिर्फ error_log में
  • attackers के लिए कुछ काम का नहीं
Code वही, safety अलग

curl से testing

curl Windows 10+, macOS और Linux के साथ आता है। Endpoint लिखते समय उसे try करने का सबसे तेज़ तरीका।

Terminal
B=http://localhost:8000/api

# 1. Public endpoints
curl $B/home.php
curl "$B/products.php?q=watch&sort=price_asc"

# 2. Login and keep the token in a variable (needs jq: apt/brew install jq)
TOKEN=$(curl -s -X POST $B/auth/login.php -H 'Content-Type: application/json' \
  -d '{"email":"test@karteasy.in","password":"Test@123"}' | jq -r .data.token)

# 3. Protected endpoints
curl -X POST $B/cart/add.php -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"product_id":7,"qty":1}'
curl $B/cart/list.php -H "Authorization: Bearer $TOKEN"

# 4. See the HTTP status too
curl -s -o /dev/null -w "%{http_code}\n" $B/product.php?id=999   # 404

Postman से testing

  1. 1KartEasy collection और variable base = http://localhost:8000/api बनाएँ
  2. 2POST {{base}}/auth/login.php जोड़ें, Body → raw → email और password वाला JSON
  3. 3Request के Tests tab में: pm.collectionVariables.set("token", pm.response.json().data.token)
  4. 4Collection → Authorization → Bearer Token → {{token}} (हर request inherit करेगी)
  5. 5बाकी endpoints जोड़ें, फिर Run collection से सब order में test करें
पाँच steps में reusable Postman collection
🔒 Postman – KartEasyPOST {{base}}/cart/add.php Send ▶Authorization: Bearer {{token}} Body: {"product_id":7,"qty":1}200 OK · 41 ms · "message": "Added to cart.""item_count": 1, "subtotal": 1899, "total": 1899

Automatic smoke test (26 checks)

हर change के बाद हाथ से endpoints click करना slow है। tests/smoke-test.php पूरी customer journey चलाकर हर जवाब check करता है। यह सिर्फ file_get_contents use करता है, इसलिए जहाँ PHP चले वहाँ चलता है — कोई extra library नहीं।

PHP – backend/tests/smoke-test.php
<?php
/*
 * KartEasy API smoke test – runs a full shopping flow and checks every answer.
 *
 *   php tests/smoke-test.php http://localhost:8000
 *   php tests/smoke-test.php https://yourdomain.com/karteasy
 *
 * It creates a new test customer each time (test+<time>@example.com), so it is
 * safe to run again and again. Run it from the command line only.
 */
if (PHP_SAPI !== 'cli') {
    http_response_code(404);
    exit;
}

$base = rtrim(isset($argv[1]) ? $argv[1] : 'http://localhost:8000', '/') . '/api/';
$token = null;
$passed = 0;
$failed = 0;

/** Sends one request and returns array(httpStatus, decodedJson). */
function call($method, $path, $body = null)
{
    global $base, $token;
    $headers = array('Accept: application/json');
    if ($body !== null) {
        $headers[] = 'Content-Type: application/json';
    }
    if ($token !== null) {
        $headers[] = 'Authorization: Bearer ' . $token;
    }
    $context = stream_context_create(array('http' => array(
        'method' => $method,
        'header' => implode("\r\n", $headers),
        'content' => $body === null ? '' : json_encode($body),
        'ignore_errors' => true,      // also read the body of 4xx / 5xx answers
        'timeout' => 15,
    )));
    $raw = @file_get_contents($base . $path, false, $context);
    $status = 0;
    if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
        $status = (int) $m[1];
    }
    return array($status, json_decode((string) $raw, true));
}

function check($label, $ok, $extra = '')
{
    global $passed, $failed;
    if ($ok) {
        $passed++;
        echo "  PASS  $label\n";
    } else {
        $failed++;
        echo "  FAIL  $label  $extra\n";
    }
}

echo "KartEasy API test → $base\n\n";

list($s, $r) = call('GET', '');
check('API is running and DB connected', $s === 200 && $r['data']['database'] === 'connected');

list($s, $r) = call('GET', 'home.php');
check('home.php has banners, categories and products', $s === 200 && count($r['data']['banners']) > 0 && count($r['data']['new_arrivals']) > 0);
$productId = $r['data']['new_arrivals'][0]['id'];

list($s, $r) = call('GET', 'products.php?q=a&sort=price_asc&limit=5');
$prices = array();
foreach ($r['data']['items'] as $p) {
    $prices[] = $p['price'];
}
$sorted = $prices;
sort($sorted);
check('products.php search sorted by price', $s === 200 && $prices === $sorted);

list($s, $r) = call('GET', 'product.php?id=999999');
check('unknown product gives 404', $s === 404 && $r['success'] === false);

list($s, $r) = call('GET', 'cart/list.php');
check('cart without token gives 401', $s === 401);

$email = 'test+' . time() . '@example.com';
list($s, $r) = call('POST', 'auth/register.php', array('name' => 'Smoke Test', 'email' => $email, 'phone' => '9876501234', 'password' => 'secret12'));
check('register a new customer', $s === 200 && strlen($r['data']['token']) === 64, json_encode($r));
$token = $r['data']['token'];

list($s, $r) = call('POST', 'auth/register.php', array('name' => 'Smoke Test', 'email' => $email, 'password' => 'secret12'));
check('same email again gives 409', $s === 409);

list($s, $r) = call('GET', 'auth/me.php');
check('me.php returns the new user', $s === 200 && $r['data']['email'] === $email);

list($s, $r) = call('POST', 'cart/add.php', array('product_id' => $productId, 'qty' => 1));
check('add to cart', $s === 200 && $r['data']['item_count'] === 1);

list($s, $r) = call('POST', 'cart/add.php', array('product_id' => $productId, 'qty' => 50));
check('too many gives 409', $s === 409);

list($s, $r) = call('POST', 'cart/update.php', array('product_id' => $productId, 'qty' => 2));
$cart = $r['data'];
check('update quantity to 2', $s === 200 && $cart['item_count'] === 2);
check('total = subtotal - discount + delivery', abs($cart['total'] - ($cart['subtotal'] - $cart['discount'] + $cart['delivery_fee'])) < 0.01);

list($s, $r) = call('POST', 'wishlist/toggle.php', array('product_id' => $productId));
check('wishlist toggle on', $s === 200 && $r['data']['in_wishlist'] === true);
list($s, $r) = call('POST', 'wishlist/toggle.php', array('product_id' => $productId));
check('wishlist toggle off', $s === 200 && $r['data']['in_wishlist'] === false);

list($s, $r) = call('POST', 'address/save.php', array('name' => 'Smoke Test', 'phone' => '12345', 'line1' => 'x', 'city' => 'y', 'state' => 'z', 'pincode' => '452001'));
check('bad phone gives 422', $s === 422);

list($s, $r) = call('POST', 'address/save.php', array('name' => 'Smoke Test', 'phone' => '9876501234', 'line1' => '1 Test Street', 'city' => 'Indore', 'state' => 'Madhya Pradesh', 'pincode' => '452001'));
check('save address (first one is default)', $s === 200 && $r['data']['is_default'] === true);
$addressId = $r['data']['id'];

list($s, $r) = call('POST', 'coupons/apply.php', array('code' => 'NO-SUCH-CODE'));
check('invalid coupon gives 422', $s === 422);

list($s, $r) = call('POST', 'orders/place.php', array('address_id' => $addressId, 'coupon' => ''));
check('place order', $s === 200 && strpos($r['data']['order_no'], 'KE') === 0, json_encode($r));
$orderId = $r['data']['order_id'];

list($s, $r) = call('GET', 'cart/list.php');
check('cart is empty after the order', $s === 200 && count($r['data']['items']) === 0);

list($s, $r) = call('GET', 'orders/detail.php?id=' . $orderId);
check('order detail has items and timeline', $s === 200 && count($r['data']['items']) === 1 && count($r['data']['timeline']) === 4);

list($s, $r) = call('POST', 'reviews/add.php', array('product_id' => $productId, 'rating' => 5, 'comment' => 'test'));
check('review before delivery gives 403', $s === 403);

list($s, $r) = call('POST', 'orders/cancel.php', array('id' => $orderId));
check('cancel order (stock goes back)', $s === 200 && $r['data']['status'] === 'cancelled');

list($s, $r) = call('POST', 'orders/cancel.php', array('id' => $orderId));
check('cancel again gives 409', $s === 409);

list($s, $r) = call('GET', 'orders/detail.php?id=1');
check('cannot open another user\'s order (404)', $s === 404 || ($s === 200 && $orderId === 1));

list($s, $r) = call('POST', 'auth/logout.php', array());
check('logout', $s === 200);
list($s, $r) = call('GET', 'auth/me.php');
check('old token no longer works', $s === 401);

echo "\n$passed passed, $failed failed\n";
exit($failed > 0 ? 1 : 0);
Terminal: php tests/smoke-test.php http://localhost:8000
KartEasy API test → http://127.0.0.1:8200/api/ PASS API is running and DB connected PASS home.php has banners, categories and products PASS products.php search sorted by price PASS unknown product gives 404 PASS cart without token gives 401 PASS register a new customer PASS same email again gives 409 PASS me.php returns the new user PASS add to cart PASS too many gives 409 PASS update quantity to 2 PASS total = subtotal - discount + delivery PASS wishlist toggle on PASS wishlist toggle off PASS bad phone gives 422 PASS save address (first one is default) PASS invalid coupon gives 422 PASS place order PASS cart is empty after the order PASS order detail has items and timeline PASS review before delivery gives 403 PASS cancel order (stock goes back) PASS cancel again gives 409 PASS cannot open another user's order (404) PASS logout PASS old token no longer works 26 passed, 0 failed
💡 हर change के बाद चलाएँ

Query बदली? Smoke test चलाएँ। कुछ FAIL हुआ तो तुरंत पता चल जाएगा कि कौन-सा flow टूटा — users से पहले।

और hardening ideas

  • API में login rate limit: login_attempts table (email, ip, time) और 15 मिनट में 5 failures के बाद मना।
  • Register पर पहले order से पहले email verification / OTP।
  • Refresh tokens: छोटे access tokens और एक लंबा refresh token।
  • Daily database backup (ज़्यादातर Hostinger plans में होता है — on है check करें)।

अक्सर पूछे जाने वाले प्रश्न (FAQ)

अगर API सब check करती है तो क्या HTTPS सच में ज़रूरी है?
हाँ। HTTPS के बिना उसी Wi-Fi पर कोई भी token और password पढ़ सकता है। Hostinger और ज़्यादातर hosts free SSL देते हैं; Android भी default रूप से plain HTTP block करता है।
क्या कोई मेरा APK decompile करके API पर attack कर सकता है?
किसी भी APK से API URL पढ़ा जा सकता है, इसलिए app में कभी secrets न रखें। KartEasy safe है क्योंकि हर rule (price, stock, ownership, review के लिए delivery) server पर check होता है।
क्या smoke test live site पर चलाऊँ?
चला सकते हैं — यह अलग test customer बनाता है और अपना order cancel करता है, इसलिए stock वापस आ जाता है। बाद में phpMyAdmin में test users delete कर दें (email test+ से शुरू)।
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

अगला topicApp Setup: Gradle, Packages और MVVM आपकी प्रगति: 12 / 28 (43%) · Lesson 13 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें