FlawOpen

flawopen.com/sql-injection/Java

● CWE-89 · अति गंभीर
सुरक्षा अनुसंधान · FlawOpen

Java SQL Injection: JDBC, JPA और MyBatis

Java SQL Injection तब होती है जब input query में जोड़ा जाता है। मानों को bind करें और ORDER BY जैसे identifiers को allowlist से सीमित करें。[1], [2], [3], [7], [4], [6]

💡 आसान भाषा में (ELI5)

ऐसी फैक्ट्री सोचिए जो ग्राहकों के लेबल आने से पहले धातु का साँचा लॉक कर देती है। JDBC PreparedStatement पहले ? वाला स्लॉट-साँचा बनाता है; फिर setLong(1, userId) निर्देश बदले बिना मान को स्लॉट में रखता है। तैयारी से पहले text concatenate करने पर वह साँचे में खुद जाता है। JdbcTemplate और JPQL setParameter भी मान अलग रखते हैं। MyBatis का अंतर स्पष्ट है: #{status} bound value है, जबकि ${status} जोड़ा गया SQL text है। ORDER BY fields identifiers हैं, values नहीं; उन्हें allowlist या स्वीकृत Sort.by property से map करें।[2], [3], [7], [4], [8]

इस पेज के मुख्य शब्द

PreparedStatement
? मार्कर वाले SQL से, कोई भी मान मिलने से पहले, कंपाइल किया गया JDBC स्टेटमेंट। मान setLong(1, id) जैसे टाइप्ड setter से आते हैं और क्वेरी को कभी नहीं बदल सकते। prepareStatement("..." + id) हमलावर का टेक्स्ट कंपाइल कर देता है और कुछ नहीं बचाता।[2], [1]
JdbcTemplate query(sql, args)
Spring के queryForList(sql, Long.class, status) और update(sql, args...) आख़िरी आर्ग्युमेंट को PreparedStatement के ज़रिए ? मार्कर से बाँधते हैं। NamedParameterJdbcTemplate यही काम :name मार्कर से करता है। sql आर्ग्युमेंट में स्ट्रिंग जोड़ना यह सब बेकार कर देता है।[3], [2]
JPQL / HQL injection
JPA और Hibernate की क्वेरी भी स्ट्रिंग से पार्स होती हैं। createQuery("... = '" + email + "'") इनपुट को JPQL बदलने देता है, जिसे Hibernate फिर SQL में बदलता है। :email के साथ setParameter या Criteria API इस्तेमाल करें।[7], [1]
MyBatis #{} vs ${}
MyBatis मैपर में #{status} एक बाइंड किया हुआ ? पैरामीटर बनता है। ${status} SQL में टेक्स्ट की तरह चिपका दिया जाता है। ${} सिर्फ़ उन मानों के लिए सुरक्षित है जिन्हें आपने ख़ुद चुना है, जैसे अनुमति-सूची का कोई कॉलम।[4]
Spring Data Sort
Spring Data Sort.by("email") को मैप की गई प्रॉपर्टी पर ORDER BY में बदलता है और अनजान नाम ठुकरा देता है, इसलिए हाथ से ORDER BY बनाने के बजाय इसे चुनें। फिर भी रिक्वेस्ट की कुंजियों को अनुमति-सूची से मिलाएँ, ताकि कोई उन फ़ील्ड पर सॉर्ट न कर सके जिन्हें आप दिखाना नहीं चाहते थे।[8]

फ्रेमवर्क मैपिंग

हमले का चरण-दर-चरण प्रवाह

चरण 1

रिक्वेस्ट का मान रिपॉज़िटरी तक पहुँचता है

Spring का @GetMapping हैंडलर @RequestParam String id लेता है और उसे findEmail() को देता है। घोषित प्रकार String होने के कारण Spring मान को किसी दूसरे प्रकार में नहीं बदलता; उसके प्रारूप पर भरोसा करने से पहले स्पष्ट रूप से जाँचें।[9]

चरण 2

स्ट्रिंग जोड़कर SQL बनता है

"... WHERE id = " + userId एक ही स्ट्रिंग बनाता है। हानिरहित टेस्ट मान 0 OR 1=1 के साथ यह WHERE id = 0 OR 1=1 बन जाता है।[1], [2], [6]

चरण 3

ड्राइवर हमलावर का टेक्स्ट कंपाइल करता है

createStatement().executeQuery(sql) पूरी स्ट्रिंग पार्स होने भेज देता है। कोई पैरामीटर नहीं है, इसलिए डेटाबेस OR 1=1 को WHERE क्लॉज़ का हिस्सा मानता है।[2], [1]

चरण 4

हर पंक्ति लौट आती है

शर्त हर पंक्ति के लिए सही है, इसलिए खोज पूरी टेबल लौटा देती है। JPQL में भी यही होता है: Hibernate बदली हुई क्वेरी को ईमानदारी से SQL में बदल देता है।[7], [1]

सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन

कमज़ोर कार्यान्वयन
// UserRepository.java: नीचे की हर क्वेरी रिक्वेस्ट डेटा को क्वेरी टेक्स्ट में जोड़ती है
public class UserRepository {
    private final DataSource dataSource;
    private final JdbcTemplate jdbc;
    private final EntityManager em;

    public UserRepository(DataSource dataSource, JdbcTemplate jdbc, EntityManager em) {
        this.dataSource = dataSource;
        this.jdbc = jdbc;
        this.em = em;
    }

    // JDBC: Statement जोड़ी गई स्ट्रिंग को मान समेत पार्स करता है
    public String findEmail(String userId) throws SQLException {
        try (Connection conn = dataSource.getConnection();
             Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery("SELECT email FROM users WHERE id = " + userId)) {
            return rs.next() ? rs.getString(1) : null;
        }
    }

    // JdbcTemplate: Spring के SQL देखने से पहले ही स्ट्रिंग जुड़ चुकी है
    public List<Long> findOrderIds(String status) {
        return jdbc.queryForList("SELECT id FROM orders WHERE status = '" + status + "'", Long.class);
    }

    // JPA: JPQL ख़ुद इनपुट से बनता है, फिर SQL में बदलता है
    public List<User> findByEmail(String email) {
        return em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'", User.class)
                 .getResultList();
    }

    // ORDER BY फ़ील्ड सीधे रिक्वेस्ट से
    public List<User> listUsers(String sortField) {
        return em.createQuery("SELECT u FROM User u ORDER BY u." + sortField, User.class)
                 .getResultList();
    }
}
सुरक्षित और सुदृढ़ फ़िक्स
// UserRepository.java: मान बाइंड पैरामीटर हैं, सॉर्ट फ़ील्ड अनुमति-सूची से आते हैं
public class UserRepository {
    private static final Map<String, String> SORTED_QUERIES = Map.of(
            "id", "SELECT u FROM User u ORDER BY u.id",
            "email", "SELECT u FROM User u ORDER BY u.email",
            "created", "SELECT u FROM User u ORDER BY u.createdAt");

    private final DataSource dataSource;
    private final JdbcTemplate jdbc;
    private final EntityManager em;

    public UserRepository(DataSource dataSource, JdbcTemplate jdbc, EntityManager em) {
        this.dataSource = dataSource;
        this.jdbc = jdbc;
        this.em = em;
    }

    // JDBC: पहले ? मार्कर कंपाइल, फिर टाइप्ड setter से मान
    public String findEmail(long userId) throws SQLException {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement("SELECT email FROM users WHERE id = ?")) {
            ps.setLong(1, userId);
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getString(1) : null;
            }
        }
    }

    // JdbcTemplate: आख़िरी आर्ग्युमेंट ? मार्कर से बँधते हैं
    public List<Long> findOrderIds(String status) {
        return jdbc.queryForList("SELECT id FROM orders WHERE status = ?", Long.class, status);
    }

    // JPA: नामित पैरामीटर, मान setParameter से बँधता है
    public List<User> findByEmail(String email) {
        return em.createQuery("SELECT u FROM User u WHERE u.email = :email", User.class)
                 .setParameter("email", email)
                 .getResultList();
    }

    // अनुमति-सूची से मिलान; क्वेरी में सिर्फ़ स्थिर JPQL टुकड़े जाते हैं
    public List<User> listUsers(String sortKey) {
        String jpql = SORTED_QUERIES.getOrDefault(sortKey, SORTED_QUERIES.get("id"));
        return em.createQuery(jpql, User.class)
                 .getResultList();
    }
}

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

स्रोत