rzo1 opened a new issue, #2096:
URL: https://github.com/apache/stormcrawler/issues/2096

   
   ## What happens
   `IndexerBolt.buildQuery()` builds the `INSERT ... ON DUPLICATE KEY UPDATE` 
statement by concatenating the labels returned by `filterMetadata()` into the 
column list and into the update clause. Values are bound as parameters, but the 
identifiers are neither validated nor quoted. With an explicit alias mapping 
the label is operator-chosen and fine. With a glob mapping such as 
`indexer.md.mapping: ["parse.*"]` the label is the raw metadata key, and 
metadata keys can come from parsed page content, for example the names Tika 
copies out of `<meta name="...">` elements.
   
   ## Where
   
`external/sql/src/main/java/org/apache/stormcrawler/sql/IndexerBolt.java:172-201`,
 config key `indexer.md.mapping`.
   
   ```java
   final String columns = String.join(", ", keys);
   ...
           .map(k -> String.format(Locale.ROOT, "%s=VALUES(%s)", k, k))
   ```
   
   The labels come from `AbstractIndexerBolt.filterMetadata()` 
(core/src/main/java/org/apache/stormcrawler/indexing/AbstractIndexerBolt.java:238-281),
 which returns `matchingKey` unchanged for a glob entry.
   
   ## Why it matters
   The practical outcome today is a broken statement rather than a working 
injection: every key a glob mapping produces is dotted, for example 
`parse.title`, and unquoted MySQL reads that as `table.column`, so the INSERT 
already fails for ordinary pages. That makes the glob mapping unusable with 
this bolt, and it makes the bolt fragile: `execute()` fails the tuple without 
emitting to the status stream, so a tuple whose metadata key breaks the SQL is 
replayed for as long as the topology runs. The interpolation itself is the 
wrong shape for a value that can originate in crawled content, and should not 
be left to depend on the accident of the label being dotted.
   
   ## Reproduction
   
   Save as 
`external/sql/src/test/java/org/apache/stormcrawler/sql/IndexerBoltQueryBuildingTest.java`.
   
   ```java
   /*
    * Licensed to the Apache Software Foundation (ASF) under one or more
    * contributor license agreements.  See the NOTICE file distributed with
    * this work for additional information regarding copyright ownership.
    * The ASF licenses this file to you under the Apache License, Version 2.0
    * (the "License"); you may not use this file except in compliance with
    * the License.  You may obtain a copy of the License at
    *
    *      http://www.apache.org/licenses/LICENSE-2.0
    *
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    * See the License for the specific language governing permissions and
    * limitations under the License.
    */
   
   package org.apache.stormcrawler.sql;
   
   import static org.junit.jupiter.api.Assertions.assertFalse;
   
   import java.lang.reflect.Method;
   import java.util.ArrayList;
   import java.util.HashMap;
   import java.util.List;
   import java.util.Map;
   import org.apache.storm.task.OutputCollector;
   import org.apache.stormcrawler.TestOutputCollector;
   import org.apache.stormcrawler.TestUtil;
   import org.apache.stormcrawler.indexing.AbstractIndexerBolt;
   import org.junit.jupiter.api.Test;
   
   /**
    * The labels returned by filterMetadata() end up in the column list and in 
the ON DUPLICATE KEY
    * UPDATE clause of the generated statement. With a glob mapping the label 
is the raw metadata key,
    * which can come from parsed content. buildQuery() neither validates nor 
quotes it.
    */
   class IndexerBoltQueryBuildingTest {
   
       private String buildQuery(List<String> keys) throws Exception {
           IndexerBolt bolt = new IndexerBolt();
           Map<String, Object> conf = new HashMap<>();
           conf.put(IndexerBolt.SQL_INDEX_TABLE_PARAM_NAME, "content");
           conf.put(AbstractIndexerBolt.urlFieldParamName, "url");
           List<String> mdMapping = new ArrayList<>();
           mdMapping.add("parse.*");
           conf.put(AbstractIndexerBolt.metadata2fieldParamName, mdMapping);
           bolt.prepare(
                   conf, TestUtil.getMockedTopologyContext(), new 
OutputCollector(new TestOutputCollector()));
           Method m = IndexerBolt.class.getDeclaredMethod("buildQuery", 
List.class);
           m.setAccessible(true);
           return (String) m.invoke(bolt, keys);
       }
   
       /** A dotted key, which is what a glob mapping such as parse.* produces 
on every ordinary page. */
       @Test
       void dottedLabelIsQuoted() throws Exception {
           String query = buildQuery(List.of("parse.title"));
           System.out.println(query);
           assertFalse(
                   query.contains("parse.title") && 
!query.contains("`parse.title`"),
                   "dotted label must be quoted or rejected, otherwise MySQL 
reads it as table.column");
       }
   
       /** A key containing SQL punctuation, which a parser can copy out of a 
crawled page. */
       @Test
       void punctuatedLabelIsNotInterpolated() throws Exception {
           String label = "a), (b";
           String query = buildQuery(List.of(label));
           System.out.println(query);
           assertFalse(
                   query.contains(label),
                   "label with punctuation must not be interpolated verbatim 
into the statement");
       }
   }
   ```
   
   Run it:
   
   ```
   mvn -pl external/sql test -Dtest=IndexerBoltQueryBuildingTest
   ```
   
   It calls `buildQuery` by reflection, so it needs no database. It fails on 
main and becomes the regression test after the fix.
   
   ```
   INSERT INTO content (url, parse.title)
   VALUES (?, ?)
   ON DUPLICATE KEY UPDATE parse.title=VALUES(parse.title)
   
   INSERT INTO content (url, a), (b)
   VALUES (?, ?)
   ON DUPLICATE KEY UPDATE a), (b=VALUES(a), (b)
   
   [ERROR] Tests run: 2, Failures: 2, Errors: 0, Skipped: 0
   [ERROR]   IndexerBoltQueryBuildingTest.dottedLabelIsQuoted:60 dotted label 
must be quoted or rejected, otherwise MySQL reads it as table.column ==> 
expected: <false> but was: <true>
   [ERROR]   IndexerBoltQueryBuildingTest.punctuatedLabelIsNotInterpolated:71 
label with punctuation must not be interpolated verbatim into the statement ==> 
expected: <false> but was: <true>
   ```
   
   ## Suggested fix
   In `IndexerBolt.buildQuery`, check every label against a strict identifier 
pattern such as `^[A-Za-z0-9_]+$` and quote it with backticks before it goes 
into the column list and the update clause. Drop labels that do not match, and 
log them once per key rather than per tuple, so an awkward metadata name cannot 
fail the tuple forever. Note that this changes behaviour for anyone relying on 
a glob mapping: those labels are dotted and do not currently work, so dropping 
them is a change from a permanent failure to a skipped column. If dotted labels 
should be supported, map them to a column name explicitly, for instance by 
replacing the dots, and document the mapping.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to