API Security Testing with Postman: A Practical Guide
APIs are where most modern application logic lives — and where most security vulnerabilities occur. REST APIs, GraphQL endpoints, and WebSocket connections are all attack surfaces that need explicit security testing.
Postman is a tool most developers and QA engineers already have. Beyond basic functional testing, it's surprisingly capable for security testing: you can write test scripts, chain requests, use environment variables, and automate repetitive checks.
This guide shows you how to use Postman for API security testing — from auth bypass to injection to rate limiting.
Setting Up for Security Testing
Create a Security Testing Environment
In Postman, create a separate environment for security testing with these variables:
base_url: https://api.example.com
admin_token: <your-admin-jwt>
user_token: <your-regular-user-jwt>
another_user_token: <token-for-a-second-test-account>
admin_user_id: 1001
regular_user_id: 1002
another_user_id: 1003Having multiple user accounts is essential for authorization testing. Create test accounts with different permission levels before you start.
Capture Your API Traffic
The fastest way to map an API's attack surface:
- Set Postman as your browser proxy: Settings → Proxy → Custom Proxy → 127.0.0.1:5559
- Browse the application normally — Postman captures all API calls
- In Postman, use History to review captured requests
- Import interesting requests into a security testing collection
Alternatively, if the API has an OpenAPI/Swagger spec, import it directly: Import → Link → paste spec URL. This gives you a complete list of endpoints to test.
Testing Authentication
Test 1: Missing Authentication
For every endpoint, verify that unauthenticated requests are rejected.
// Postman Pre-request Script — remove auth header
pm.request.headers.remove('Authorization');Or create a duplicate request with the Authorization header removed. The test:
pm.test("Unauthenticated request rejected", function() {
pm.expect(pm.response.code).to.be.oneOf([401, 403]);
});Send to every endpoint. Any 200 response without authentication is a vulnerability.
Test 2: JWT Vulnerabilities
If your API uses JWTs, decode the token (jwt.io or Postman's built-in JWT decode):
// In Postman test script — decode and inspect JWT
const token = pm.environment.get('user_token');
const payload = JSON.parse(atob(token.split('.')[1]));
console.log(payload);
// Check for weak algorithm
pm.test("JWT does not use 'none' algorithm", function() {
const header = JSON.parse(atob(token.split('.')[0]));
pm.expect(header.alg).to.not.equal('none');
pm.expect(header.alg).to.not.equal('HS256'); // weak — prefer RS256
});Algorithm confusion test: Some servers accept alg: none (no signature) or can be tricked into verifying an HMAC signature using the public key as the secret.
Craft a modified JWT:
- Take a valid JWT
- Change the payload (elevate role to admin)
- Change the header
algtonone - Remove the signature
- Send the request — does it work?
Header: {"alg":"none","typ":"JWT"}
Payload: {"sub":"1002","role":"admin","exp":9999999999}
Signature: (empty)
→ Token: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMDAyIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.Expected: 401 Unauthorized. Fail: request succeeds with elevated privileges.
Test 3: Token Expiry
// Save an old token, wait for it to expire, then test
const expiredToken = 'eyJhbGci...'; // token saved from a previous session
pm.request.headers.upsert({
key: 'Authorization',
value: `Bearer ${expiredToken}`
});Test:
pm.test("Expired token rejected", function() {
pm.expect(pm.response.code).to.equal(401);
});Test 4: API Key Handling
If the API uses API keys:
// Test API key in different locations
// Some APIs accept keys in headers, query params, or body — check all
pm.test("API key required in header", function() {
// Send request without X-API-Key header
pm.expect(pm.response.code).to.equal(401);
});Also test:
- What happens with an invalid key? Does error message reveal key format/length?
- Is the key visible in server logs? (Send key in URL param — servers log URLs)
- Is the key rotatable? Does the old key expire after rotation?
Testing Authorization (IDOR and Privilege Escalation)
Test 5: Horizontal IDOR
The most common API vulnerability. User A accessing User B's data.
Create a Postman collection runner that cycles through resource IDs:
// Pre-request Script
const ids = [1001, 1002, 1003, 1004, 1005];
const currentIndex = pm.environment.get('idor_index') || 0;
pm.environment.set('current_id', ids[currentIndex]);
pm.environment.set('idor_index', parseInt(currentIndex) + 1);Request:
GET {{base_url}}/api/users/{{current_id}}/profile
Authorization: Bearer {{user_token}} // User 1002's tokenTest:
pm.test("Cannot access other users' profiles", function() {
const currentId = pm.environment.get('current_id');
const myId = pm.environment.get('regular_user_id');
if (currentId !== myId) {
// Requesting someone else's profile — should fail
pm.expect(pm.response.code).to.be.oneOf([403, 404]);
} else {
// Own profile — should succeed
pm.expect(pm.response.code).to.equal(200);
}
});Run this collection with iterations set to 5. Any 200 response on a non-owned resource = confirmed IDOR.
Test 6: Vertical Privilege Escalation
Can a regular user access admin-only endpoints?
// Use regular user token against admin endpoints
// These requests should all return 403
const adminEndpoints = [
'/api/admin/users',
'/api/admin/settings',
'/api/admin/reports',
'/api/users/delete',
];Create requests for each admin endpoint using the user_token (not admin_token).
pm.test("Regular user cannot access admin endpoint", function() {
pm.expect(pm.response.code).to.equal(403);
});Test 7: Mass Assignment
Some APIs expose all model properties in update endpoints, including ones that shouldn't be user-modifiable.
// Normal request
PATCH /api/users/1002
{"name": "New Name", "email": "new@example.com"}
// Attack — add fields the user shouldn't be able to modify
PATCH /api/users/1002
{
"name": "New Name",
"email": "new@example.com",
"role": "admin",
"credits": 999999,
"verified": true,
"subscription": "enterprise"
}Test:
pm.test("Extra fields ignored or rejected", function() {
const response = pm.response.json();
// Verify role didn't change
pm.expect(response.role).to.not.equal('admin');
pm.expect(response.credits).to.not.equal(999999);
});Testing Input Validation and Injection
Test 8: SQL Injection
Create a request variable {{injection_payload}} and test each parameter:
Common payloads to test:
'
''
`
''`
' OR '1'='1
' OR '1'='1'--
' OR '1'='1'/*
) OR ('1'='1
'; DROP TABLE users--
1 UNION SELECT null,null,null--// Pre-request Script — cycle through injection payloads
const payloads = ["'", "' OR '1'='1", "'; SELECT 1--", "1 UNION SELECT null--"];
const idx = pm.environment.get('inj_index') || 0;
pm.environment.set('injection_payload', payloads[idx]);
pm.environment.set('inj_index', parseInt(idx) + 1);Request:
GET {{base_url}}/api/products?search={{injection_payload}}Tests:
pm.test("No SQL error in response", function() {
const body = pm.response.text();
const sqlErrors = ['SQL syntax', 'ORA-', 'PostgreSQL', 'mysql_fetch',
'You have an error', 'sqlite3.', 'SQLSTATE'];
sqlErrors.forEach(err => {
pm.expect(body).to.not.include(err);
});
});
pm.test("No unexpected 500 errors on injection attempt", function() {
pm.expect(pm.response.code).to.not.equal(500);
});Test 9: NoSQL Injection
If the API uses MongoDB or another NoSQL database:
// Instead of: {"username": "admin", "password": "test"}
// Try:
{
"username": "admin",
"password": {"$gt": ""}
}
// Or in URL params:
// ?username=admin&password[$gt]=pm.test("NoSQL injection rejected", function() {
pm.expect(pm.response.code).to.not.equal(200);
// Or verify you didn't get authenticated
pm.expect(pm.response.json()).to.not.have.property('token');
});Test 10: Command Injection in File Operations
For any endpoint that takes filenames, paths, or system commands:
filename=test.pdf
filename=test.pdf; whoami
filename=../../../etc/passwd
filename=test%00.pdfpm.test("Command injection blocked", function() {
pm.expect(pm.response.code).to.not.equal(200);
pm.expect(pm.response.text()).to.not.include('root:');
pm.expect(pm.response.text()).to.not.include('/bin/bash');
});Testing Rate Limiting
Test 11: Brute Force Protection
// Run in a Collection Runner with 50 iterations
pm.test("Rate limiting kicks in after repeated failures", function() {
const iteration = pm.info.iteration;
if (iteration > 10) {
// After 10 attempts, expect 429 Too Many Requests
pm.expect(pm.response.code).to.equal(429);
}
});Check response headers for rate limit info:
pm.test("Rate limit headers present", function() {
pm.expect(pm.response.headers.get('X-RateLimit-Limit')).to.exist;
pm.expect(pm.response.headers.get('X-RateLimit-Remaining')).to.exist;
pm.expect(pm.response.headers.get('Retry-After')).to.exist;
});Test 12: Resource Exhaustion
Test what happens with unusually large inputs:
// Pre-request Script — send large payload
const largeString = 'A'.repeat(100000);
pm.request.body.update({
mode: 'raw',
raw: JSON.stringify({search: largeString})
});Expected: 400 Bad Request or 413 Payload Too Large. Fail: server hangs, returns 500, or processes the request.
Testing Information Disclosure
Test 13: Verbose Error Messages
Trigger errors intentionally:
// Wrong types
GET /api/products/abc (where integer expected)
GET /api/products/-1
GET /api/products/99999999999999
// Missing required fields
POST /api/orders {}
// Malformed JSON
POST /api/orders
Content-Type: application/json
Body: {invalid json}Tests:
pm.test("No stack traces in error responses", function() {
const body = pm.response.text();
pm.expect(body).to.not.include('at Object.'); // Node.js stack trace
pm.expect(body).to.not.include('Traceback'); // Python
pm.expect(body).to.not.include('Exception in'); // Java
pm.expect(body).to.not.include('/app/server.js'); // File paths
});
pm.test("Error response uses standard format", function() {
const response = pm.response.json();
// Should have error message but not internal details
pm.expect(response).to.have.property('error');
pm.expect(response).to.not.have.property('stack');
pm.expect(response).to.not.have.property('query'); // No raw SQL
});Test 14: Sensitive Data in Responses
pm.test("Password not returned in user object", function() {
const response = pm.response.json();
pm.expect(response).to.not.have.property('password');
pm.expect(response).to.not.have.property('password_hash');
pm.expect(response).to.not.have.property('salt');
});
pm.test("Full credit card number not returned", function() {
const body = pm.response.text();
// Credit card pattern
pm.expect(body).to.not.match(/\b\d{16}\b/);
});Organizing Security Tests in Postman
Create a Security folder in your Postman collection with subfolders:
Security/
├── Authentication/
│ ├── Unauthenticated access
│ ├── Expired token
│ ├── JWT algorithm none
│ └── Invalid token formats
├── Authorization/
│ ├── IDOR - user resources
│ ├── IDOR - admin endpoints
│ └── Mass assignment
├── Injection/
│ ├── SQL injection scan
│ ├── NoSQL injection
│ └── Command injection
├── Rate Limiting/
│ ├── Login brute force
│ └── API abuse
└── Information Disclosure/
├── Error messages
└── Sensitive data in responsesRunning in CI with Newman
Export your Postman collection and run it in CI with Newman:
npm install -g newman
# Run security test collection
newman run security-tests.postman_collection.json \
--environment security.postman_environment.json \
--reporters cli,junit \
--reporter-junit-export security-results.xmlIn GitHub Actions:
- name: API Security Tests
run: |
newman run collections/security-tests.json \
--environment environments/staging.json \
--bailThe --bail flag stops the run on first failure — useful when a security failure should block deployment.
Beyond Postman
Postman is excellent for structured security test cases. Pair it with:
- Burp Suite for intercepting and fuzzing ad-hoc during exploratory testing
- OWASP ZAP for automated scanning in CI
- sqlmap for thorough SQL injection testing
For our full penetration testing methodology, see the web app penetration testing guide. For integrating security scans into your CI pipeline, see automated security testing in CI.
HelpMeTest can complement Postman security tests by running authentication and authorization scenarios continuously against your staging environment — catching regressions as soon as they're deployed.