Skip to content
Open

Sec #11

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 83 additions & 20 deletions .github/workflows/ai-security-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,11 @@ name: Claude Security Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: only run when security-sensitive areas change
# paths:
# - "src/auth/**"
# - "src/api/**"
# - "infra/**"
# - "config/**"

jobs:
security:
runs-on: ubuntu-latest

# Least privilege for PR review + required OIDC for the Claude GitHub App
permissions:
contents: read
pull-requests: write
Expand All @@ -24,7 +17,84 @@ jobs:
- name: Checkout PR
uses: actions/checkout@v6
with:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:large_orange_circle: MEDIUM — Security Control Modified by the PR Under Review

Issue: This PR modifies the very workflow that performs security review. An attacker (or a developer with write access) could weaken or disable security checks — remove severity categories, drop findings, whitelist vulnerabilities — and the weakened policy would govern the review of the same PR that introduced the change.

Risk: Security review becomes self-referentially ineffective. This is analogous to asking a contractor to write their own safety inspection checklist.

Fix: Protect .github/workflows/ with a CODEOWNERS rule requiring approval from a dedicated security team member before workflow changes can be merged:

# .github/CODEOWNERS
.github/workflows/ai-security-review.yml @org/security-team

Consider also using pull_request_target for the security review trigger so it always runs from the base branch policy, not the PR branch — though note pull_request_target has its own risks and requires careful scoping.

Reference: OWASP CI/CD Security Top 10 — CICD-SEC-4 (Poisoned Pipeline Execution)

fetch-depth: 1
fetch-depth: 2

- name: Generate Security Policy Instructions
run: |
cat <<'EOF' > security_policy.md
You are a Principal Application Security Engineer performing a security-focused code review.

## CRITICAL – Block PR if found:
- Hardcoded secrets, tokens, credentials
- SQL Injection
- Remote Code Execution
- Path traversal
- Unsafe deserialization
- Missing authentication/authorization on sensitive endpoints
- Sensitive data exposure

## HIGH – Must Fix:
- XSS
- SSRF
- IDOR
- CSRF missing protections
- XXE
- Weak cryptography
- Race conditions affecting security

## MEDIUM – Fix before production:
- Missing input validation
- Overly permissive CORS
- Missing security headers
- Missing rate limiting
- Dependency vulnerabilities
- Insufficient logging

## Technology Specific Checks

### Python
- eval(), exec()
- pickle/yaml.load unsafe
- subprocess shell=True
- missing authentication decorators
- weak password hashing
- JWT validation flaws

### JavaScript/TypeScript
- innerHTML with user input
- eval()/Function()
- insecure postMessage
- localStorage for sensitive data
- exposed API keys

### Infrastructure (Terraform)
- 0.0.0.0/0 ingress
- "*" IAM policies
- unencrypted storage
- public S3 buckets
- missing logging

## Output Format

For each finding:

:red_circle: CRITICAL | :large_yellow_circle: HIGH | :large_orange_circle: MEDIUM | :large_blue_circle: LOW

**Issue:**
**Location:** File:Line
**Risk:**
**Fix:**
**Reference:** CWE/OWASP if applicable

## Summary

- Security Score: PASS or FAIL
- Count by severity
- Must-fix before merge
- Positive security practices observed

Focus strictly on security. Ignore style or performance unless security-related.
EOF

- name: Claude security review
uses: anthropics/claude-code-action@v1
Expand All @@ -34,20 +104,13 @@ jobs:
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}

Perform a security-focused review. Prioritize:
- AuthN/AuthZ issues (BOLA/BFLA/BOPLA)
- Injection (SQLi/NoSQLi/Command)
- SSRF, deserialization, crypto misuse
- Sensitive data exposure, logging issues
- Security misconfig in code/config
Below is the mandatory security review policy to follow strictly:

$(cat security_policy.md)

For each finding, include:
Severity: CRITICAL/HIGH/MEDIUM/LOW
What/Where
Impact
Recommendation
Analyze ALL changes in this PR and produce a structured security review according to the policy above.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:large_orange_circle: MEDIUM — Shell Substitution $(cat ...) Does Not Execute in with: Blocks

Issue: $(cat security_policy.md) is inside a GitHub Actions with: input. GitHub Actions processes ${{ }} expressions in with: fields, but not POSIX shell command substitution ($()). The Claude action will receive the literal string $(cat security_policy.md) — the policy content will never be injected.

Risk: The security review runs with no effective policy guidance, silently degrading review quality while appearing functional.

Fix: Use a prior step to capture the file contents and expose it as a step output or environment variable, then reference it with ${{ steps.policy.outputs.content }}:

- name: Load security policy
  id: policy
  run: echo "content=$(cat security_policy.md)" >> $GITHUB_OUTPUT

- name: Claude security review
  with:
    prompt: |
      ${{ steps.policy.outputs.content }}
      ...

Or inline the policy directly into the prompt: block and eliminate the file-generation step entirely.


Use inline comments for line-level issues and one top-level summary comment.

claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
2 changes: 1 addition & 1 deletion badApi/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const router = express.Router();
// In-memory database for storing user information (including sensitive data)
const usersInfo = [
{ username: 'user1', dob: '1990-01-01', address: '5, 3rd mainland Bridge', cardInfo: '1234-5678-9012-3456', children: ['Child1', 'Child2'], balance: 1000, role: 'user' },
{ username: 'user2', dob: '1985-05-15', address: '7, Ghost street', cardInfo: '9876-5432-1098-7654', children: ['Child3'], balance: 500, role: 'user' },
{ username: 'user3', dob: '1985-05-15', address: '7, Ghost street', cardInfo: '9876-5432-1098-7654', children: ['Child3'], balance: 500, role: 'user' },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: HIGH — Hardcoded PII and Credit Card Numbers in Source Code + Data Inconsistency Introduced by This PR

This line contains a full, unmasked credit card number (9876-5432-1098-7654) and personally identifiable information (DoB, address, children's names) embedded directly in source code.

Additionally, this PR renames the username from user2 to user3 here, while admin.js still references user2 for the same logical record. This data inconsistency means the admin loan-grant endpoint and account endpoint now operate on different user data sets.

Impact:

  • Real card numbers / PII in source code are exposed to every developer, CI system, and anyone with repo access — violating PCI-DSS and data minimisation requirements
  • Inconsistent test data can mask authorization bypass bugs in testing

Recommendation:

  • Replace all hardcoded sensitive values with clearly fake placeholders (e.g., XXXX-XXXX-XXXX-1234)
  • Keep seed data consistent across modules, or share a single source-of-truth fixture file

Comment on lines 6 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL — Hardcoded PII and Payment Card Data in Source Code

Issue: Full credit card numbers (cardInfo), dates of birth, home addresses, and family information are hardcoded as static in-memory data in source code.

Risk:

  • PCI-DSS violation: Full PANs (Primary Account Numbers) in source code is an automatic PCI-DSS Level 1 failure. Even "test" card numbers can constitute a violation if they look like real card numbers.
  • GDPR/privacy violation: Real-looking PII (DOB, addresses) in committed code can constitute sensitive data exposure (Article 5 GDPR).
  • This data is permanently in git history; removing it from the file is insufficient.

Fix: Use obviously fake, clearly-marked test data that cannot be confused with real credentials:

// SAFE TEST DATA — clearly synthetic, not real PAN format
{ username: 'user1', dob: 'XXXX-XX-XX', address: 'TEST_ADDRESS', cardInfo: 'TEST-CARD-XXXX', ... }

Never store card numbers — even partial — in application code. Use a tokenization service for any payment card reference.

Reference: CWE-312 (Cleartext Storage of Sensitive Information) | OWASP A02:2021 | PCI-DSS Requirement 3

];

/**
Expand Down
2 changes: 1 addition & 1 deletion badApi/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const router = express.Router();
const users = []; // Simulated user storage for the sake of example
const usersInfo = [
{ username: 'user1', dob: '1990-01-01', address: '5, 3rd mainland Bridge', cardInfo: '1234-5678-9012-3456', children: ['Child1', 'Child2'], balance: 1000, role: 'user' },
{ username: 'user2', dob: '1985-05-15', address: '7, Ghost street', cardInfo: '9876-5432-1098-7654', children: ['Child3'], balance: 500, role: 'user' },
{ username: 'user2', dob: '1985-05-17', address: '7, Ghost street', cardInfo: '9876-5432-1098-7654', children: ['Child3'], balance: 500, role: 'user' },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: CRITICAL — Broken Function-Level Authorization on Admin Endpoints

This PR changes a DoB in the seed data, but the critical issue is the surrounding module: POST /admin/hidden-admin-signup (line 150) has no authentication whatsoever. Any anonymous caller can POST /admin/hidden-admin-signup with any username/password and receive an admin-role account.

Similarly, POST /admin/grant-loan/:username (line 160) modifies user balances with no admin token check.

Impact: Complete privilege escalation — any unauthenticated user can become an admin, then grant themselves or others arbitrary balance increases.

Recommendation:

  • Both endpoints must require a valid admin JWT/session before execution
  • The "hidden" admin signup endpoint should be removed from the production surface and from Swagger docs entirely
  • Admin account creation should only be possible via a seeded initial admin or out-of-band provisioning

Comment on lines 7 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL — Hardcoded PII and Payment Card Data in Source Code

Issue: Same pattern as account.js — full credit card PANs, DOB, and address hardcoded in the admin module's in-memory store.

Risk: Identical to account.js: PCI-DSS violation, sensitive data exposure via git history, and GDPR risk. Admin modules often have broader data access, amplifying the exposure surface.

Fix: Replace with clearly synthetic, obviously-fake test data. Consider a shared test-fixtures module rather than duplicating data across files.

Reference: CWE-312 | OWASP A02:2021 | PCI-DSS Requirement 3

];

/**
Expand Down
2 changes: 1 addition & 1 deletion badApi/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const mysql = require('mysql2');
const connection = mysql.createConnection({
host: 'localhost',
user: 'ghost',
password: 'ghost_sec', //coming back here
password: 'ghost_sec1', //coming back here
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: CRITICAL — Hardcoded Database Credential

The password was changed from ghost_sec to ghost_sec1, but it is still hardcoded in source code. This commit has permanently embedded a real credential into git history.

Impact: Anyone with read access to the repository (or git history) gains the database password. Even if the password is rotated, the old value survives in history unless a full history rewrite is performed.

Recommendation:

  • Load credentials from environment variables: password: process.env.DB_PASSWORD
  • Rotate the database password immediately since both values are now in git history
  • Add db.js (or a .env file) to .gitignore if it ever contained real secrets
  • The comment //coming back here indicates the developer knew this was a problem — this must not ship

database: 'badapi_db2'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CRITICAL — Hardcoded Database Credential

Issue: Database password is hardcoded directly in source code. Rotating from ghost_secghost_sec1 does not fix the underlying problem; both values are now in git history forever.

Risk: Anyone with read access to the repository (or any git clone / fork / leak) has the database password. Credentials committed to git cannot be truly revoked from history without a rewrite.

Fix:

Suggested change
database: 'badapi_db2'
password: process.env.DB_PASSWORD,

Store all credentials in environment variables and inject them at runtime (e.g., via .env + dotenv, GitHub Actions secrets, or a secrets manager). Rotate the current password immediately.

Reference: CWE-798 (Use of Hard-coded Credentials) | OWASP A02:2021 Cryptographic Failures

});

Expand Down
2 changes: 1 addition & 1 deletion badApi/loan.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const router = express.Router();

// In-memory database for loan applications
let loanApplications = [
{ username: 'user1', amount: 1000, status: 'pending' },
{ username: 'user1', amount: 5000, status: 'pending' },
];

/**
Expand Down
4 changes: 2 additions & 2 deletions badApi/otp.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const router = express.Router();
* properties:
* otp:
* type: integer
* example: 123456
* example: 1234567
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: LOW — Swagger Example Contradicts Implementation

The example was updated to 1234567 (7 digits), but the code on line 42 generates a 3-digit number (100–999). This creates misleading API documentation and masks the more serious 3-digit OTP weakness from API consumers.

Recommendation: Fix the underlying OTP generation (see comment on line 42), then update the example to reflect the corrected 6-digit format.

* 500:
* description: Internal server error
* content:
Expand All @@ -39,7 +39,7 @@ const router = express.Router();
*/

router.post('/otp', (req, res) => {
const otp = Math.floor(100 + Math.random() * 900); // Generate 3-digit random number
const otp = Math.floor(100 + Math.random() * 900); // Generate 3-digit random number.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: CRITICAL — Insecure OTP: Weak Entropy + Trivial Brute Force + Exposed in Response

Three compounding problems on this line:

  1. Math.random() is not cryptographically secure. It is predictable and unsuitable for security tokens. Use crypto.randomInt() from Node's built-in crypto module.

  2. Only 3 digits (100–999 = 900 possible values). An attacker can exhaust the entire OTP space in seconds with no rate limiting in place. A minimum of 6 digits is required (1,000,000 combinations).

  3. OTP is returned directly in the API response. This completely negates its purpose — the OTP must be delivered via an out-of-band channel (SMS, email, authenticator app) and only a success/failure response should be returned to the caller.

Impact: OTP can be trivially predicted or brute-forced, enabling account takeover of any user.

Recommendation:

const otp = crypto.randomInt(100000, 999999); // 6-digit CSPRNG OTP
// Send via SMS/email; do NOT return in response
res.json({ message: 'OTP sent' });

Add per-IP and per-user rate limiting (e.g., max 5 attempts, 15-minute lockout).

return res.json({ otp });
Comment on lines +42 to 43
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:large_yellow_circle: HIGH — Weak OTP: Non-Cryptographic Randomness + Trivial Brute-Force Surface + Secret Returned to Caller

Issue (Line 42) — CWE-338: Math.random() is a pseudo-random number generator (PRNG) seeded predictably by the JavaScript runtime. It is not cryptographically secure. An attacker who can observe timing or output patterns may predict future OTPs.

Issue (Line 42) — 3-digit space: Math.floor(100 + Math.random() * 900) produces only 900 possible values (100–999). Even with a perfect PRNG, this is trivially exhausted by brute force in under a second.

Issue (Line 43) — Sensitive Data Exposure: The OTP is returned directly in the HTTP response to the requester. A valid OTP flow sends the secret out-of-band (SMS, email, authenticator app) to a verified channel — it must never be returned to the API caller, or it provides zero authentication value.

Fix:

const { randomInt } = require('crypto');

router.post('/otp', (req, res) => {
  // 6-digit, cryptographically secure OTP — sent out-of-band, NOT returned
  const otp = randomInt(100000, 999999);
  // sendOtpViaSms(req.user.phone, otp);  // deliver to verified channel
  return res.json({ message: 'OTP sent' }); // never return the OTP value
});

Also add rate limiting (e.g., 3 attempts per 15 minutes) and expiry (e.g., 5-minute TTL).

Reference: CWE-338 (Weak PRNG) | CWE-330 (Insufficient Randomness) | OWASP A02:2021 | NIST SP 800-63B

});

Expand Down
2 changes: 1 addition & 1 deletion badApi/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ app.use(bodyParser.json());
// Swagger setup
const swaggerOptions = {
swaggerDefinition: {
openapi: '3.0.0',
openapi: '3.0.1',
info: {
title: 'BadAPI Documentation',
version: '1.0.0',
Expand Down