Repository navigation
ACOMMONS-217 Introduce shared IP address extraction with IpAddressExtractor - #573
lijun-chen-sonarsource wants to merge 12 commits into
Conversation
fb5fb16 to
874419b
Compare
IpAddressExtractor
c218252 to
de9f436
Compare
de9f436 to
77e3a1a
Compare
guillem-bartrina-sonarsource
left a comment
There was a problem hiding this comment.
I believe it is a good idea to standardize the extraction of IP addresses from literals. However, I am not convinced of the coupling with the IpAddressClassifier. I believe there should be a separation of concerns.
Note that ParsedIp is an internal class of the latter, produced by its parse functionality. The extractor is concerned with extracting the slice corresponding to an IP address from a more complex structure. It just so happens to wrap its final result with a call to parse. Notice that you had to add the literal field in ParsedIp just to be able to retrieve the result of the extractor.
To me, it would be clearer if the extractor just extracted the IP address from the literal; the users of the API could then call the classifier if they want to classify it. It is a simple composition on the user's side, and you could remove the literal field from ParsedIp.
WDYT?
|
Thanks for the thorough review.
I agree, I didn’t like the coupling either. Given the name To avoid duplicating IP address validation logic, I reused One option would be to extract the validation and parsing logic, along with the parsed representation, into a separate class shared by A simpler alternative would be to name it I'm inclined to go with the latter, though it feels uglier. WDYT? |
I'd go with the latter as well, even if it is a bit ugly. Maybe @teemu-rytilahti-sonarsource has some input. |
…inning and bails out on any error
There was a problem hiding this comment.
Just a couple of nits from a very quick review, but it is looking good already, thanks @lijun-chen-sonarsource!
The PR title and the desc need an update after the refactoring & renaming.
| private static final Pattern URL_PREFIX = Pattern.compile( | ||
| "^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?//(?:[^/?#,@\\s]*@)?", Pattern.UNICODE_CHARACTER_CLASS); |
There was a problem hiding this comment.
It is probably worth to limit the maximum number of matching characters for the scheme part. https://www.iana.org/assignments/uri-schemes contains the available, IANA-registered schemes, so maybe picking up the longest one and adding some extra characters (+10 or so) would make sense?
If this is only used for IP addresses, the part after // could also be stricter, if domain names are also wanted, the matching could be done by restricting the match length between // and the next non-dot and non-dash word boundary.
There was a problem hiding this comment.
We support matching also user information (e.g., http://user@1.2.3.4/path), for which there is AFAIK no official hard limit. So, in general we can't bound the regex match before reaching to a IP address host (unless we want to put some arbitrary custom limit).
Given this, I find bounding the scheme also not that useful. The scan is O(n) in time and O(1) in memory. I don't see strong reason to bound it.
WDYT?
| @ParameterizedTest | ||
| @CsvSource({ | ||
| // Bare addresses and endpoints | ||
| "1.2.3.4, 1.2.3.4", |
There was a problem hiding this comment.
1.2.3.4 is located in a reserved address space, it is better to use something non-routable (e.g., from the documentation networks) for both v4 and v6 examples.
There was a problem hiding this comment.
Addressed in 4f9e405
| class IpAddressClassifierTest { | ||
|
|
||
| @ParameterizedTest | ||
| @ValueSource(strings = {"2.5.255.255", "10.1.2.3/8", "::FFFF:127.0.0.1", "FE80::1%eth0/64"}) |
There was a problem hiding this comment.
2.5/16 is an assigned range, see my earlier comment on using example/doc ranges.
| private static final Pattern URL_PREFIX = Pattern.compile( | ||
| "^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?//(?:[^/?#,@\\s]*@)?", Pattern.UNICODE_CHARACTER_CLASS); | ||
| private static final Pattern ADDRESS_DELIMITER = Pattern.compile("[?#,\\s]", Pattern.UNICODE_CHARACTER_CLASS); | ||
| private static final Pattern ASCII_DIGITS = Pattern.compile("\\d+"); |
There was a problem hiding this comment.
CIDR should be up to max 2 (for v4) and 3 (v6) digits.
There was a problem hiding this comment.
ASCII_DIGITS is used both both CIDR (2 or 3 digits) and port (5 digits).
I kept it simple and did length checking before doing the regex matching.
See 135abfd
| "[2003::1]:65535, 2003::1", | ||
| // URL hosts, including scheme-relative URLs and mixed notation | ||
| "http://1.2.3.4:8080/path, 1.2.3.4", | ||
| "http://1.2.3.4/path with space, 1.2.3.4", |
There was a problem hiding this comment.
It would be a good idea to have have few test cases for query strings containing ampersands, semi-colons, and similar characters that are found in URLs in the wild.
There was a problem hiding this comment.
Addressed in 36c5f6f
guillem-bartrina-sonarsource
left a comment
There was a problem hiding this comment.
LGTM from my side, after addressing the other review comments
…ssClassifier are decoupled
|
| * | ||
| * <ul> | ||
| * <li>Skips URL user information before the host, such as {@code user@} or {@code user:pass@}.</li> | ||
| * <li>Removes a single enclosing pair of brackets from IPv6 addresses, and ASCII decimal ports from 0 through 65535. |
There was a problem hiding this comment.
💡 Quality: Javadoc omits the new five-character port limit
isPort now rejects any port string longer than 5 characters before parsing it, so 192.0.2.1:000080 returns empty even though its value, 80, is in range. The new test at line 161 checks for this. The Javadoc for extract still says ports are accepted when they are "ASCII decimal ports from 0 through 65535", which tells callers that zero-padded ports like 000080 will be stripped. Callers in PHP, Go and Kotlin analyzers that rely on the documented contract will get Optional.empty() for these inputs. The fix is to state the limit in the doc, e.g. "ASCII decimal ports of at most five digits with values from 0 through 65535".
Document the five-digit bound on ports:
* <li>Removes a single enclosing pair of brackets from IPv6 addresses, and ASCII decimal ports of at most five digits
* with values from 0 through 65535.
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
Code Review 👍 Approved with suggestions 2 closed / 3 findings🟡 Medium risk · The shared heuristic extractor selects URL hosts and first-list candidates, so ambiguous inputs could cause analyzers to miss or misclassify IP findings. Adds One open minor finding: the 💡 Quality: Javadoc omits the new five-character port limit📄 commons/src/main/java/org/sonarsource/analyzer/commons/appsec/IpAddressCandidateExtractor.java:47 📄 commons/src/main/java/org/sonarsource/analyzer/commons/appsec/IpAddressCandidateExtractor.java:131-136 📄 commons/src/test/java/org/sonarsource/analyzer/commons/appsec/IpAddressCandidateExtractorTest.java:161
Document the five-digit bound on ports✅ 2 closed✅ Edge Case: Endpoint with port treats a numeric path as a CIDR suffix
✅ Edge Case: Unlisted delimiters cut off malformed candidates, so 1.2.3.4~foo is accepted
🤖 Prompt for agentsVerification Rules 2/2 met Repos 12 inspected · PRs 2 inspectedFunctional validation No results Rules 2 passed 1 not applicable
Broader impact Repos 12 inspected PRs 2 inspectedCross-repo 12 repositories inspected 0 open impacts
7 more repositories Cross-PR 2 PRs inspected 0 open impacts
Auto-approval Not enabledFor more verification details, comment OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |




Add
IpAddressExtractorso PHP, Go, Kotlin, and other analyzers can reuse consistent extraction behavior for bare addresses, endpoints, and URLs.The implementation is extracted from sonar-php PR #1848, with these additions:
Optional<ParsedIp>, allowing callers to query classification directly.ParsedIp.literal()preserves the extracted spelling for analyzer-specific filters.+,-, and., and skip optional URL user information such asuser:pass@.1.2.3.4:8080/33yields1.2.3.4, while bare CIDRs retain their prefix for validation and classification.Stack created with GitHub Stacks CLI • Give Feedback 💬
Summary by Gitar
IpAddressExtractortoIpAddressCandidateExtractorand removed its dependency onIpAddressClassifierextractreturnsOptional<String>candidates instead ofOptional<ParsedIp>, leaving validation toIpAddressClassifier.parseThis will update automatically on new commits.